diff --git a/packages/angular/material-error/package.json b/packages/angular/material-error/package.json
index 39d50cb..a4921fe 100644
--- a/packages/angular/material-error/package.json
+++ b/packages/angular/material-error/package.json
@@ -6,10 +6,12 @@
"@angular/common": ">=14.1.0 < 18.0.0",
"@angular/core": ">=14.1.0 < 18.0.0",
"@angular/forms": ">=14.1.0 < 18.0.0",
- "@angular/material": ">=14.1.0 < 18.0.0",
"@angular/platform-browser": ">=14.1.0 < 18.0.0",
"rxjs": ">=6.0.0 < 8.0.0"
},
+ "optionalDependencies": {
+ "@angular/material": ">=14.1.0 < 18.0.0"
+ },
"dependencies": {
"tslib": "^2.3.0"
},
diff --git a/packages/angular/material-error/src/lib/dynamic-error.directive.spec.ts b/packages/angular/material-error/src/lib/dynamic-error.directive.spec.ts
index edf024d..8fefcaf 100644
--- a/packages/angular/material-error/src/lib/dynamic-error.directive.spec.ts
+++ b/packages/angular/material-error/src/lib/dynamic-error.directive.spec.ts
@@ -5,96 +5,312 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { Component, ViewChild } from '@angular/core';
import {
FormControl,
+ FormGroup,
+ FormsModule,
ReactiveFormsModule,
ValidationErrors,
Validators,
} from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
+import { fireEvent } from '@testing-library/dom';
import { ErrorMessageProvider } from './error-message-provider';
import { DynamicErrorDirective } from './dynamic-error.directive';
-@Component({
- template: `
-
-
- `,
- standalone: true,
- imports: [
- DynamicErrorDirective,
- MatInputModule,
- MatFormFieldModule,
- ReactiveFormsModule,
- ],
-})
-class DirectiveTestComponent {
- @ViewChild(DynamicErrorDirective, { static: true }) directive:
- | DynamicErrorDirective
- | undefined;
- public readonly formControl = new FormControl('', Validators.required);
-}
-
-const render = async () => {
- await TestBed.configureTestingModule({
- imports: [DirectiveTestComponent, NoopAnimationsModule],
- providers: [
- {
- provide: ErrorMessageProvider,
- useValue: {
- getErrorMessagesFor: (error: ValidationErrors) =>
- Object.keys(error)[0],
- },
- },
- ],
- }).compileComponents();
-
- const fixture = TestBed.createComponent(DirectiveTestComponent);
- fixture.detectChanges();
- const loader = TestbedHarnessEnvironment.loader(fixture);
- return { fixture, loader };
-};
-
describe('DynamicErrorDirective', () => {
- it('should create', async () => {
- const { fixture } = await render();
- expect(fixture.componentInstance).toBeTruthy();
- });
+ describe('Reactive form with material input', () => {
+ @Component({
+ template: `
`,
+ standalone: true,
+ imports: [
+ DynamicErrorDirective,
+ MatInputModule,
+ MatFormFieldModule,
+ ReactiveFormsModule,
+ ],
+ })
+ class DirectiveTestComponent {
+ @ViewChild(DynamicErrorDirective, { static: true }) directive:
+ | DynamicErrorDirective
+ | undefined;
+ public readonly formControl = new FormControl('', Validators.required);
+ public readonly formGroup = new FormGroup({
+ formControl: this.formControl,
+ });
+ }
+
+ const render = async () => {
+ await TestBed.configureTestingModule({
+ imports: [DirectiveTestComponent, NoopAnimationsModule],
+ providers: [
+ {
+ provide: ErrorMessageProvider,
+ useValue: {
+ getErrorMessagesFor: (error: ValidationErrors) =>
+ Object.keys(error)[0],
+ },
+ },
+ ],
+ }).compileComponents();
+
+ const fixture = TestBed.createComponent(DirectiveTestComponent);
+ fixture.detectChanges();
+ const loader = TestbedHarnessEnvironment.loader(fixture);
+ return { fixture, loader };
+ };
+ it('should create', async () => {
+ const { fixture } = await render();
+ expect(fixture.componentInstance).toBeTruthy();
+ });
+
+ it('should show error message', async () => {
+ const { fixture, loader } = await render();
+ const input = await loader.getHarness(MatInputHarness);
+
+ await input.focus();
+ await input.setValue('a');
+ await input.setValue('');
+ await input.blur();
+
+ expect(fixture.nativeElement.textContent).toContain('required');
+ });
+
+ it('should hide error message', async () => {
+ const { fixture, loader } = await render();
+ const input = await loader.getHarness(MatInputHarness);
+
+ await input.focus();
+ await input.setValue('a');
+ await input.setValue('');
+ await input.setValue('a');
+ await input.blur();
+
+ expect(fixture.nativeElement.textContent).not.toContain('required');
+ });
- it('should show error message', async () => {
- const { fixture, loader } = await render();
- const input = await loader.getHarness(MatInputHarness);
+ it('should unsubscribe in on destroy', async () => {
+ const { fixture, loader } = await render();
+ const input = await loader.getHarness(MatInputHarness);
- await input.focus();
- await input.setValue('a');
- await input.setValue('');
- await input.blur();
+ fixture.componentInstance.directive?.ngOnDestroy();
+ await input.focus();
+ await input.setValue('a');
+ await input.setValue('');
+ await input.blur();
- expect(fixture.nativeElement.textContent).toContain('required');
+ expect(fixture.nativeElement.textContent).not.toContain('required');
+ });
+
+ it('should show error message on submit', async () => {
+ const { fixture } = await render();
+
+ const form = fixture.nativeElement.querySelector('form');
+ fireEvent.submit(form);
+
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).toContain('required');
+ });
});
- it('should hide error message', async () => {
- const { fixture, loader } = await render();
- const input = await loader.getHarness(MatInputHarness);
+ describe('Form with material input', () => {
+ @Component({
+ template: ``,
+ standalone: true,
+ imports: [
+ DynamicErrorDirective,
+ MatInputModule,
+ MatFormFieldModule,
+ FormsModule,
+ ],
+ })
+ class DirectiveTestComponent {
+ @ViewChild(DynamicErrorDirective, { static: true }) directive:
+ | DynamicErrorDirective
+ | undefined;
+ public value = '';
+ }
+
+ const render = async () => {
+ await TestBed.configureTestingModule({
+ imports: [DirectiveTestComponent, NoopAnimationsModule],
+ providers: [
+ {
+ provide: ErrorMessageProvider,
+ useValue: {
+ getErrorMessagesFor: (error: ValidationErrors) =>
+ Object.keys(error)[0],
+ },
+ },
+ ],
+ }).compileComponents();
+
+ const fixture = TestBed.createComponent(DirectiveTestComponent);
+ fixture.detectChanges();
+ const loader = TestbedHarnessEnvironment.loader(fixture);
+ return { fixture, loader };
+ };
+ it('should create', async () => {
+ const { fixture } = await render();
+ expect(fixture.componentInstance).toBeTruthy();
+ });
+
+ it('should show error message', async () => {
+ const { fixture, loader } = await render();
+ const input = await loader.getHarness(MatInputHarness);
+
+ await input.focus();
+ await input.setValue('a');
+ await input.setValue('');
+ await input.blur();
+
+ expect(fixture.nativeElement.textContent).toContain('required');
+ });
+
+ it('should hide error message', async () => {
+ const { fixture, loader } = await render();
+ const input = await loader.getHarness(MatInputHarness);
- await input.focus();
- await input.setValue('a');
- await input.setValue('');
- await input.setValue('a');
- await input.blur();
+ await input.focus();
+ await input.setValue('a');
+ await input.setValue('');
+ await input.setValue('a');
+ await input.blur();
- expect(fixture.nativeElement.textContent).not.toContain('required');
+ expect(fixture.nativeElement.textContent).not.toContain('required');
+ });
+
+ it('should unsubscribe in on destroy', async () => {
+ const { fixture, loader } = await render();
+ const input = await loader.getHarness(MatInputHarness);
+
+ fixture.componentInstance.directive?.ngOnDestroy();
+ await input.focus();
+ await input.setValue('a');
+ await input.setValue('');
+ await input.blur();
+
+ expect(fixture.nativeElement.textContent).not.toContain('required');
+ });
+
+ it('should show error message on submit', async () => {
+ const { fixture } = await render();
+
+ const form = fixture.nativeElement.querySelector('form');
+ fireEvent.submit(form);
+
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).toContain('required');
+ });
});
- it('should unsubscribe in on destroy', async () => {
- const { fixture, loader } = await render();
- const input = await loader.getHarness(MatInputHarness);
+ describe('Form control bare', () => {
+ @Component({
+ template: ``,
+ standalone: true,
+ imports: [DynamicErrorDirective, ReactiveFormsModule],
+ })
+ class DirectiveTestComponent {
+ @ViewChild(DynamicErrorDirective, { static: true }) directive:
+ | DynamicErrorDirective
+ | undefined;
+ public readonly formControl = new FormControl('', Validators.required);
+ public readonly formGroup = new FormGroup({
+ formControl: this.formControl,
+ });
+ }
+
+ const render = async () => {
+ await TestBed.configureTestingModule({
+ imports: [DirectiveTestComponent, NoopAnimationsModule],
+ providers: [
+ {
+ provide: ErrorMessageProvider,
+ useValue: {
+ getErrorMessagesFor: (error: ValidationErrors) =>
+ Object.keys(error)[0],
+ },
+ },
+ ],
+ }).compileComponents();
+
+ const fixture = TestBed.createComponent(DirectiveTestComponent);
+ fixture.detectChanges();
+ return { fixture };
+ };
+ it('should create', async () => {
+ const { fixture } = await render();
+ expect(fixture.componentInstance).toBeTruthy();
+ });
+
+ it('should show error message', async () => {
+ const { fixture } = await render();
+ const input = fixture.nativeElement.querySelector('input');
+
+ fireEvent.focus(input);
+ fireEvent.input(input, { target: { value: 'a' } });
+ fixture.detectChanges();
+ fireEvent.input(input, { target: { value: '' } });
+ fireEvent.blur(input);
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).toContain('required');
+ });
+
+ it('should hide error message', async () => {
+ const { fixture } = await render();
+ const input = fixture.nativeElement.querySelector('input');
+
+ fireEvent.focus(input);
+ fireEvent.input(input, { target: { value: 'a' } });
+ fixture.detectChanges();
+ fireEvent.input(input, { target: { value: '' } });
+ fixture.detectChanges();
+ fireEvent.input(input, { target: { value: 'a' } });
+ fireEvent.blur(input);
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).not.toContain('required');
+ });
+
+ it('should unsubscribe in on destroy', async () => {
+ const { fixture } = await render();
+ const input = fixture.nativeElement.querySelector('input');
+
+ fixture.componentInstance.directive?.ngOnDestroy();
+ fireEvent.focus(input);
+ fireEvent.input(input, { target: { value: 'a' } });
+ fixture.detectChanges();
+ fireEvent.input(input, { target: { value: '' } });
+ fireEvent.blur(input);
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).not.toContain('required');
+ });
+
+ it('should show error message on submit', async () => {
+ const { fixture } = await render();
+
+ const form = fixture.nativeElement.querySelector('form');
+ fireEvent.submit(form);
- fixture.componentInstance.directive?.ngOnDestroy();
- await input.focus();
- await input.setValue('a');
- await input.setValue('');
- await input.blur();
+ fixture.detectChanges();
- expect(fixture.nativeElement.textContent).not.toContain('required');
+ expect(fixture.nativeElement.textContent).toContain('required');
+ });
});
});
diff --git a/packages/angular/material-error/src/lib/dynamic-error.directive.ts b/packages/angular/material-error/src/lib/dynamic-error.directive.ts
index a989cfe..cf9d257 100644
--- a/packages/angular/material-error/src/lib/dynamic-error.directive.ts
+++ b/packages/angular/material-error/src/lib/dynamic-error.directive.ts
@@ -2,13 +2,20 @@ import {
AfterContentInit,
ChangeDetectorRef,
Directive,
+ Input,
OnDestroy,
Renderer2,
TemplateRef,
ViewContainerRef,
inject,
+ isDevMode,
} from '@angular/core';
-import { FormGroupDirective, NgForm, ValidationErrors } from '@angular/forms';
+import {
+ AbstractControl,
+ FormGroupDirective,
+ NgForm,
+ ValidationErrors,
+} from '@angular/forms';
import { MatFormField } from '@angular/material/form-field';
import { merge, Subscription } from 'rxjs';
import { ErrorMessageProvider } from './error-message-provider';
@@ -18,12 +25,16 @@ import { ErrorMessageProvider } from './error-message-provider';
standalone: true,
})
export class DynamicErrorDirective implements AfterContentInit, OnDestroy {
+ @Input({
+ alias: 'schamanDynamicError',
+ })
+ private formControl: AbstractControl | undefined;
private text?: Text;
private subscription?: Subscription;
private readonly templateReference = inject(TemplateRef);
private readonly viewContainer = inject(ViewContainerRef);
private readonly renderer = inject(Renderer2);
- private readonly matFormField = inject(MatFormField);
+ private readonly matFormField = inject(MatFormField, { optional: true });
private readonly errorMessageProvider = inject(ErrorMessageProvider);
private readonly ngForm = inject(NgForm, { optional: true });
private readonly formGroupDirective = inject(FormGroupDirective, {
@@ -31,9 +42,22 @@ export class DynamicErrorDirective implements AfterContentInit, OnDestroy {
});
private readonly changeDetectorReference = inject(ChangeDetectorRef);
public ngAfterContentInit(): void {
- const matFormFieldControl = this.matFormField._control;
- const ngControl = matFormFieldControl.ngControl;
- const observables = [matFormFieldControl.stateChanges];
+ const observables = [];
+ if (!this.matFormField && !this.formControl) {
+ if (isDevMode()) {
+ console.warn(
+ 'No form control provided. Please provide a form control or a mat form field.',
+ );
+ }
+ }
+ const matFormFieldControl = this.matFormField?._control;
+ const ngControl = matFormFieldControl?.ngControl;
+ if (matFormFieldControl) {
+ observables.push(matFormFieldControl.stateChanges);
+ }
+ if (this.formControl) {
+ observables.push(this.formControl.statusChanges);
+ }
if (this.ngForm) {
observables.push(this.ngForm.ngSubmit);
}
@@ -41,8 +65,9 @@ export class DynamicErrorDirective implements AfterContentInit, OnDestroy {
observables.push(this.formGroupDirective.ngSubmit);
}
this.subscription = merge(...observables).subscribe(() => {
- if (matFormFieldControl.errorState && ngControl?.errors) {
- this.addText(ngControl.errors);
+ const errors = this.formControl?.errors ?? ngControl?.errors;
+ if ((matFormFieldControl?.errorState ?? true) && errors) {
+ this.addText(errors);
} else if (this.text) {
this.removeText();
}