Skip to content

Commit

Permalink
fix(components/forms): refactor form errors (#1981) (#1988)
Browse files Browse the repository at this point in the history
  • Loading branch information
blackbaud-sky-build-user authored Feb 5, 2024
1 parent 38f0fdb commit 915a64a
Show file tree
Hide file tree
Showing 24 changed files with 528 additions and 237 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,17 @@ <h2>New member form</h2>
</sky-input-box>
</sky-column>
<sky-column [screenSmall]="12" [screenMedium]="6">
<sky-input-box labelText="Last name" stacked="true">
<input formControlName="lastName" spellcheck="false" type="text" />
<sky-input-box
data-sky-id="input-box-last-name"
labelText="Last name"
stacked="true"
>
<input
class="last-name-input-box"
formControlName="lastName"
spellcheck="false"
type="text"
/>
</sky-input-box>
</sky-column>
</sky-row>
Expand Down Expand Up @@ -69,18 +78,14 @@ <h2>New member form</h2>
<option value="green">Green</option>
<option value="blue">Blue</option>
<option value="purple">Purple</option>
<option value="bird">Bird</option>
<option value="invalid">Invalid Color</option>
</select>
<!-- Custom validator not handled by input box. -->
<div class="sky-error-indicator">
<sky-status-indicator
*ngIf="favoriteColor.errors?.['color'] as colorError"
descriptionType="error"
indicatorType="danger"
>
{{ colorError.message }}
</sky-status-indicator>
</div>
<!-- Custom form error not handled by input box. -->
<sky-form-error
*ngIf="favoriteColor.errors?.['invalid']"
errorName="invalid"
errorText="Invalid Color is not a color"
/>
</sky-input-box>
</sky-column>
</sky-row>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SkyAppTestUtility } from '@skyux-sdk/testing';
import { SkyInputBoxHarness } from '@skyux/forms/testing';

import { DemoComponent } from './demo.component';
Expand Down Expand Up @@ -40,6 +41,22 @@ describe('Basic input box demo', () => {
});
});

describe('last name field', () => {
it('should have last name required', async () => {
const harness = await setupTest({
dataSkyId: 'input-box-last-name',
});
const inputEl = document.querySelector(
'input.last-name-input-box',
) as HTMLInputElement;
inputEl.value = '';
SkyAppTestUtility.fireDomEvent(inputEl, 'input');
SkyAppTestUtility.fireDomEvent(inputEl, 'blur');

await expectAsync(harness.hasRequiredError()).toBeResolvedTo(true);
});
});

describe('bio field', () => {
it('should have a character limit of 250', async () => {
const harness = await setupTest({
Expand Down Expand Up @@ -71,7 +88,7 @@ describe('Basic input box demo', () => {
});

describe('favorite color field', () => {
it('should not allow bird to be selected', async () => {
it('should not allow invalid color to be selected', async () => {
const harness = await setupTest({
dataSkyId: 'input-box-favorite-color',
});
Expand All @@ -80,19 +97,11 @@ describe('Basic input box demo', () => {
'.input-box-favorite-color-select',
) as HTMLSelectElement;

selectEl.value = 'bird';
selectEl.value = 'invalid';
selectEl.dispatchEvent(new Event('change'));

const customErrors = await harness.getCustomErrors();

expect(customErrors.length).toBe(1);

const birdError = customErrors[0];

await expectAsync(birdError.getDescriptionType()).toBeResolvedTo('error');
await expectAsync(birdError.getIndicatorType()).toBeResolvedTo('danger');
await expectAsync(birdError.getText()).toBeResolvedTo(
'Bird is not a color.',
await expectAsync(harness.hasCustomFormError('invalid')).toBeResolvedTo(
true,
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,8 @@ export class DemoComponent {
constructor() {
this.favoriteColor = new FormControl('none', [
(control): ValidationErrors | null => {
if (control.value === 'bird') {
return {
color: {
invalid: true,
message: 'Bird is not a color.',
},
};
if (control.value === 'invalid') {
return { invalid: true };
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,18 @@

<div id="input-box-form-control-name-error">
<form [formGroup]="errorForm">
<sky-input-box
labelText="Custom error easy mode"
stacked="true"
hintText="type in blue"
>
<input formControlName="customError" type="text" />
<sky-form-error
*ngIf="customError.errors?.['blue']"
errorName="blue"
errorText="Input must be blue."
/>
</sky-input-box>
<sky-input-box
mode="detect"
labelText="Form control by name error with status indicator"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
NgModel,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors,
Validators,
} from '@angular/forms';

Expand All @@ -16,6 +17,8 @@ export class InputBoxComponent implements OnInit, AfterViewInit {

public errorField: UntypedFormControl;

public customError: UntypedFormControl;

public errorForm: UntypedFormGroup;

public errorNgModelValue: string;
Expand All @@ -28,10 +31,23 @@ export class InputBoxComponent implements OnInit, AfterViewInit {
public ngOnInit(): void {
this.errorField = new UntypedFormControl('', [Validators.required]);

this.customError = new UntypedFormControl('', [
(control): ValidationErrors | null => {
console.log(control.value);
if (control.value !== 'blue') {
return { blue: true };
}
return null;
},
Validators.required,
Validators.maxLength(1),
]);

this.errorField.markAsTouched();

this.errorForm = new UntypedFormGroup({
errorFormField: new UntypedFormControl('', [Validators.required]),
customError: this.customError,
});
this.errorAutofillForm = new UntypedFormGroup({
errorAutofillFormField: new UntypedFormControl('', [
Expand Down
4 changes: 4 additions & 0 deletions libs/components/forms/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { SkyCheckboxChange } from './lib/modules/checkbox/checkbox-change';
export { SkyCheckboxModule } from './lib/modules/checkbox/checkbox.module';

export { SkyFormErrorsModule } from './lib/modules/form-error/form-errors.module';
export { SkyFormErrorModule } from './lib/modules/form-error/form-error.module';

export { SkyFileAttachmentsModule } from './lib/modules/file-attachment/file-attachments.module';
export { SkyFileItem } from './lib/modules/file-attachment/file-item';
Expand All @@ -29,6 +30,8 @@ export { SkySelectionBoxGridAlignItemsType } from './lib/modules/selection-box/t
export { SkyToggleSwitchModule } from './lib/modules/toggle-switch/toggle-switch.module';
export { SkyToggleSwitchChange } from './lib/modules/toggle-switch/types/toggle-switch-change';

export { SKY_FORM_ERRORS_ENABLED } from './lib/modules/form-error/form-errors-enabled-token';

// Components and directives must be exported to support Angular's "partial" Ivy compiler.
// Obscure names are used to indicate types are not part of the public API.

Expand All @@ -41,6 +44,7 @@ export { SkyFileAttachmentComponent as λ7 } from './lib/modules/file-attachment
export { SkyFileDropComponent as λ8 } from './lib/modules/file-attachment/file-drop.component';
export { SkyFileItemComponent as λ9 } from './lib/modules/file-attachment/file-item.component';
export { SkyFormErrorsComponent as λ21 } from './lib/modules/form-error/form-errors.component';
export { SkyFormErrorComponent as λ22 } from './lib/modules/form-error/form-error.component';
export { SkyInputBoxControlDirective as λ20 } from './lib/modules/input-box/input-box-control.directive';
export { SkyInputBoxComponent as λ10 } from './lib/modules/input-box/input-box.component';
export { SkyRadioGroupComponent as λ11 } from './lib/modules/radio/radio-group.component';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { expect } from '@skyux-sdk/testing';

import { SkyFormErrorModule } from './form-error.module';
import { SKY_FORM_ERRORS_ENABLED } from './form-errors-enabled-token';
import { SkyFormErrorsModule } from './form-errors.module';

@Component({
standalone: true,
imports: [SkyFormErrorsModule, SkyFormErrorModule],
providers: [{ provide: SKY_FORM_ERRORS_ENABLED, useValue: true }],
template: `
<sky-form-error errorName="required" errorText="This field is required" />
`,
})
class FormErrorWithTokenComponent {}

@Component({
standalone: true,
imports: [SkyFormErrorsModule, SkyFormErrorModule],
template: `
<sky-form-error errorName="required" errorText="This field is required" />
`,
})
class FormErrorWithoutTokenComponent {}

describe('Form error component', () => {
it('renders an error message when form errors enabled token is provided', () => {
const fixture = TestBed.createComponent(FormErrorWithTokenComponent);

fixture.detectChanges();

expect(
fixture.nativeElement.querySelector('.sky-form-error'),
).toBeVisible();
});

it('throws an error when form errors enabled token is not provided', () => {
expect(() =>
TestBed.createComponent(FormErrorWithoutTokenComponent),
).toThrowError(
'The `sky-form-error` component is not supported in the provided context.',
);
});
});
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
Input,
inject,
} from '@angular/core';
import { SkyStatusIndicatorModule } from '@skyux/indicators';

import { SKY_FORM_ERRORS_ENABLED } from './form-errors-enabled-token';

/**
* @internal
* Displays default and custom input error messages for SKY UX form components.
*/
@Component({
selector: 'sky-form-error',
standalone: true,
imports: [SkyStatusIndicatorModule],
imports: [SkyStatusIndicatorModule, CommonModule],
template: `
<sky-status-indicator
*ngIf="formErrors"
class="sky-form-error"
descriptionType="error"
indicatorType="danger"
>
<ng-content />
{{ errorText }}
</sky-status-indicator>
`,
styles: [
Expand All @@ -28,6 +37,27 @@ import { SkyStatusIndicatorModule } from '@skyux/indicators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SkyFormErrorComponent {
@HostBinding('class')
protected readonly cssClass = 'sky-form-error-indicator';
/**
* The name of the error.
*/
@Input({ required: true })
public errorName!: string;

/**
* The error message to display.
*/
@Input({ required: true })
public errorText!: string;

protected readonly formErrors = inject(SKY_FORM_ERRORS_ENABLED, {
optional: true,
});

constructor() {
if (!this.formErrors) {
throw new Error(
'The `sky-form-error` component is not supported in the provided context.',
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NgModule } from '@angular/core';

import { SkyFormErrorComponent } from './form-error.component';

@NgModule({
imports: [SkyFormErrorComponent],
exports: [SkyFormErrorComponent],
})
export class SkyFormErrorModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { InjectionToken } from '@angular/core';

/**
* @internal
*/
export const SKY_FORM_ERRORS_ENABLED = new InjectionToken<boolean>(
'SKY_FORM_ERRORS_ENABLED',
);
Loading

0 comments on commit 915a64a

Please sign in to comment.