Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(components/forms): refactor form errors #1981

Merged
merged 3 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,15 @@ <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="blur">Blur</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?.['color']"
errorName="color"
>
Blur is not a color.
</sky-form-error>
</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 blur 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 = 'blur';
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('color')).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 === 'blur') {
return { color: true };
}

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

<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">
Input must be blue.
</sky-form-error>
</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
3 changes: 3 additions & 0 deletions libs/components/forms/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ 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 { SkyFormErrorComponent } from './lib/modules/form-error/form-error.component';
export { FORM_ERRORS } from './lib/modules/form-error/form-errors-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 Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
HostBinding,
Input,
inject,
} from '@angular/core';
import { SkyStatusIndicatorModule } from '@skyux/indicators';

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

/**
* @internal
*/
@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"
Expand All @@ -28,6 +39,26 @@ import { SkyStatusIndicatorModule } from '@skyux/indicators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SkyFormErrorComponent {
@Input()
public set errorName(value: string) {
this.#_errorName = value;
this.#updateClasses();
}

public get errorName(): string {
return this.#_errorName;
}

@HostBinding('class')
protected readonly cssClass = 'sky-form-error-indicator';
protected cssClass = '';

#_errorName = '';

protected readonly formErrors = inject(FORM_ERRORS, { optional: true });
readonly #changeDetector = inject(ChangeDetectorRef);

#updateClasses(): void {
this.cssClass = `sky-form-error-${this.errorName} sky-form-error-indicator`;
this.#changeDetector.markForCheck();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { InjectionToken } from '@angular/core';

export const FORM_ERRORS = new InjectionToken<string>('FORM_ERRORS');
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<ng-container *ngIf="labelText && showErrors && errors">
<sky-form-error *ngIf="errors['required']" class="sky-form-error-required">
<sky-form-error *ngIf="errors['required']" errorName="required">
{{ 'skyux_form_error_required' | skyLibResources : labelText }}
</sky-form-error>

<sky-form-error
*ngIf="errors['maxlength'] as maxLengthError"
class="sky-form-error-maxlength"
errorName="maxlength"
>
{{
'skyux_form_error_maxlength'
Expand All @@ -15,41 +15,31 @@

<sky-form-error
*ngIf="errors['minlength'] as minLengthError"
class="sky-form-error-minlength"
errorName="minlength"
>
{{
'skyux_form_error_minlength'
| skyLibResources : labelText : minLengthError.requiredLength
}}
</sky-form-error>

<sky-form-error
*ngIf="errors['skyCharacterCounter'] as characterCounterError"
class="sky-form-error-character-counter"
>
{{
'skyux_form_error_character_count'
| skyLibResources : labelText : characterCounterError.limit
}}
</sky-form-error>

<sky-form-error *ngIf="errors['skyDate']" class="sky-form-error-date">
<sky-form-error *ngIf="errors['skyDate']" errorName="date">
{{ 'skyux_form_error_date' | skyLibResources }}
</sky-form-error>

<sky-form-error *ngIf="errors['skyEmail']" class="sky-form-error-email">
<sky-form-error *ngIf="errors['skyEmail']" errorName="email">
{{ 'skyux_form_error_email' | skyLibResources }}
</sky-form-error>

<sky-form-error *ngIf="errors['skyPhoneField']" class="sky-form-error-phone">
<sky-form-error *ngIf="errors['skyPhoneField']" errorName="phone">
{{ 'skyux_form_error_phone' | skyLibResources }}
</sky-form-error>

<sky-form-error *ngIf="errors['skyTime']" class="sky-form-error-time">
<sky-form-error *ngIf="errors['skyTime']" errorName="time">
{{ 'skyux_form_error_time' | skyLibResources }}
</sky-form-error>

<sky-form-error *ngIf="errors['skyUrl']" class="sky-form-error-url">
<sky-form-error *ngIf="errors['skyUrl']" errorName="url">
{{ 'skyux_form_error_url' | skyLibResources }}
</sky-form-error>
</ng-container>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
:host,
sky-status-indicator {
display: block;
line-height: normal;
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
[showErrors]="controlDir?.touched || controlDir?.dirty"
>
<ng-content select=".sky-error-label,.sky-error-indicator" />
<ng-content select="sky-form-error" />
</sky-form-errors>
</ng-template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { SkyContentInfoProvider, SkyIdService } from '@skyux/core';

import { ReplaySubject } from 'rxjs';

import { FORM_ERRORS } from '../form-error/form-errors-token';

import { SkyInputBoxAdapterService } from './input-box-adapter.service';
import { SkyInputBoxControlDirective } from './input-box-control.directive';
import { SkyInputBoxHostService } from './input-box-host.service';
Expand All @@ -47,6 +49,10 @@ import { SkyInputBoxPopulateArgs } from './input-box-populate-args';
SkyContentInfoProvider,
SkyInputBoxAdapterService,
SkyInputBoxHostService,
{
provide: FORM_ERRORS,
useValue: true,
},
],
// Note that change detection is not set to OnPush; default change detection allows the
// invalid CSS class to be added when the content control's invalid/dirty state changes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { SkyThemeModule } from '@skyux/theme';

import { SkyCharacterCounterModule } from '../character-counter/character-counter.module';
import { SkyFormErrorComponent } from '../form-error/form-error.component';
import { SkyFormErrorsComponent } from '../form-error/form-errors.component';

import { SkyInputBoxControlDirective } from './input-box-control.directive';
Expand All @@ -14,11 +15,16 @@ import { SkyInputBoxComponent } from './input-box.component';
imports: [
CommonModule,
SkyCharacterCounterModule,
SkyFormErrorComponent,
SkyFormErrorsComponent,
SkyInputBoxControlDirective,
SkyInputBoxHelpInlineComponent,
SkyThemeModule,
],
exports: [SkyInputBoxComponent, SkyInputBoxControlDirective],
exports: [
SkyInputBoxComponent,
SkyInputBoxControlDirective,
SkyFormErrorComponent,
],
})
export class SkyInputBoxModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,10 @@ export class SkyFormErrorHarness extends SkyComponentHarness {
return SkyFormErrorHarness.getDataSkyIdPredicate(filters);
}

async #getFormErrorClasses(): Promise<string[]> {
const formErrorClasses = await (await this.host()).getProperty('classList');
return Array.from(formErrorClasses);
}

/*
* Gets the error class that signifies which error has fired.
* Gets the error name.
*/
public async getFirstClassError(): Promise<string> {
return (await this.#getFormErrorClasses())[0];
public async getErrorName(): Promise<string | null> {
return (await this.host()).getAttribute('errorName');
}
}
Loading
Loading