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: update test app custom error component's property names #20

Merged
merged 14 commits into from
Jul 14, 2020
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
Prev Previous commit
Next Next commit
fix(config): changes for PR #15 review feedback
  • Loading branch information
harikvpy committed Jun 29, 2020
commit da29053cd67a07f442bb482015323692968353e5
116 changes: 87 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,43 +200,101 @@ The library adds a `form-submitted` to the submitted form. You can use it to sty
}
```
- `controlErrorComponent` - Optional. Allows changing the default component that is used to reflect
the errors. This component should implement the `IControlErrorComponent` interface. Defaults to
`ControlErrorComponent`.
the errors. This component should implement the `ControlErrorComponent` interface. Defaults to
`DefaultControlErrorComponent`.

For example:
```ts
{
controlErrorComponent: CustomControlErrorComponent
}
```

`CustomControlErrorComponent` is declared as:
```ts
@Component(...)
export class CustomControlErrorComponent implements IControlErrorComponent {
// Custom error component that will replace the standard DefaultControlErrorComponent.
// Use an HTML fragment that suits your UI framework of choice. Example below shows
// error text wrapped in an <ion-item> which best serves a typical Ionic form layout
// where each form field is enclosed in an <ion-item>, all of which are wrapped in
// an <ion-list>.
@Component({
template: `
<ion-item lines='none' class='ion-text-wrap'>
<ion-label color='danger' class='ion-no-margin ion-text-wrap' stacked>
{{ _text }}
</ion-label>
</ion-item>
`
})
export class IonicControlErrorComponent implements ControlErrorComponent {

set customClass(className: string) {
this.host.nativeElement.classList.add(className);
}

set customClass(className: string) {
this.host.nativeElement.classList.add(className);
}
set text(value: string|null) {
this._text = value;
this.hide = !value;
this.cdr.markForCheck();
}
}

set text(value: string|null) {
this._text = value;
this.hide = !value;
this.cdr.markForCheck();
}
}
@NgModule({
declarations: [AppComponent, IonicControlErrorComponent],
imports: [
ReactiveFormsModule,
ErrorTailorModule.forRoot({
errors: {
useValue: {
required: 'This field is required'
}
},
controlErrorComponent: IonicControlErrorComponent
})
],
bootstrap: [AppComponent]
})
export class AppModule {}
```
- `controlErrorComponentAnchorFn` - Optional. A hook function that allows the error component to be
repositioned in the DOM. By default error components are inserted at the bottom of the form field
with error. If your UI layout dictates a different positioning scheme, you may use this hook.
- `controlErrorComponentAnchorFn` - Optional. A hook function that allows the error component's
HTML element to be repositioned in the DOM. By default error components are inserted at the
bottom of the form field with error. If your UI layout dictates a different positioning
scheme, you may use this hook.

In the following example, the error component is attached to the form field's
grandparent element.
A note about manipulating DOM directly. Since this error element can be placed anywhere in
the DOM, it also has to be removed when the error component is destroyed. To provide
for this, this function should return a function that will then be called when the
error component is destroyed. You may use this to remove the error HTML element
that you inserted into the DOM yourself.

For example:
```ts
controlErrorComponentAnchorFn(host: ElementRef, errorComponent: ComponentRef<IControlErrorComponent>) {
let errorComponentElem = (errorComponent.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
(host.nativeElement as HTMLElement).parentElement.parentElement.append(errorComponentElem);
}
// Extending the Ionic example above, since the control error element have to be
// attached to the control's grandparent, we provide a custom anchor function that
// will be called with the control's error component HTML element.
// hostElem is the HTML element for the control and errorElem is the HTML for the
// error component.
//
// The function returns a function that will be called when the error component is
// destroyed, giving you a chance to remove the HTML fragment that you added to
// the DOM earlier.
anchorIonicErrorComponent(hostElem: Element, errorElem: Element) {
hostElem.parentElement.parentElement.append(errorElem);
return () => {
hostElem.parentElement.parentElement.removeChild(errorElem);
}
}

@NgModule({
declarations: [AppComponent, IonicControlErrorComponent],
imports: [
ReactiveFormsModule,
ErrorTailorModule.forRoot({
errors: {
useValue: {
required: 'This field is required'
}
},
controlErrorComponent: IonicControlErrorComponent,
controlErrorComponentAnchorFn: anchorIonicErrorComponent
})
],
bootstrap: [AppComponent]
})
export class AppModule {}
```

### Per Control Config
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Spectator, createComponentFactory, byText } from '@ngneat/spectator';

import { ControlErrorComponent } from './control-error.component';
import { DefaultControlErrorComponent } from './control-error.component';

describe('ControlErrorComponent', () => {
let spectator: Spectator<ControlErrorComponent>;
const createComponent = createComponentFactory(ControlErrorComponent);
let spectator: Spectator<DefaultControlErrorComponent>;
const createComponent = createComponentFactory(DefaultControlErrorComponent);

beforeEach(() => (spectator = createComponent()));

Expand Down
16 changes: 12 additions & 4 deletions projects/ngneat/error-tailor/src/lib/control-error.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, TemplateRef } from '@angular/core';
import { ValidationErrors } from '@angular/forms';

export interface IControlErrorComponent {
export interface ControlErrorComponent {
customClass: string;
text: string | null;
createTemplate?(tpl: TemplateRef<any>, error: ValidationErrors, text: string): void;
createTemplate?(
tpl: TemplateRef<{ $implicit: ValidationErrors; text: string }>,
error: ValidationErrors,
text: string
): void;
}

@Component({
Expand All @@ -26,13 +30,17 @@ export interface IControlErrorComponent {
`
]
})
export class ControlErrorComponent implements IControlErrorComponent {
export class DefaultControlErrorComponent implements ControlErrorComponent {
_text: string | null = null;
_tpl: TemplateRef<{ $implicit: ValidationErrors; text: string }> | undefined;
context: { $implicit: ValidationErrors; text: string };
hide = true;

createTemplate(tpl: TemplateRef<any>, error: ValidationErrors, text: string) {
createTemplate(
tpl: TemplateRef<{ $implicit: ValidationErrors; text: string }>,
error: ValidationErrors,
text: string
) {
this._tpl = tpl;
this.context = { $implicit: error, text };
this.cdr.markForCheck();
Expand Down
119 changes: 118 additions & 1 deletion projects/ngneat/error-tailor/src/lib/control-error.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Component, Type } from '@angular/core';
import { Component, Type, ElementRef, ChangeDetectorRef } from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { byPlaceholder, byText, createComponentFactory, Spectator } from '@ngneat/spectator';
import { ErrorTailorModule } from '@ngneat/error-tailor';
import { tick, fakeAsync } from '@angular/core/testing';
import { ControlErrorComponent } from './control-error.component';

function getComponentFactory<C>(component: Type<C>) {
return createComponentFactory({
Expand Down Expand Up @@ -315,4 +316,120 @@ describe('ControlErrorDirective', () => {
});
});
});

describe('GlobalConfig', () => {
@Component({
template: `
<form [formGroup]="form" errorTailor>
<input formControlName="name" placeholder="Name" />
</form>
`
})
class CustomErrorFormGroupComponent {
form = this.builder.group({
name: new FormControl('', [Validators.required])
});

customErrors = {
required: 'required error'
};

constructor(private builder: FormBuilder) {}
}

@Component({
template: `
<h1>{{ _text }}</h1>
`
})
class CustomControlErrorComponent implements ControlErrorComponent {
_text: string | null;
hide = true;

set customClass(className: string) {
this.host.nativeElement.classList.add(className);
}

set text(value: string | null) {
if (value !== this._text) {
this._text = value;
this.hide = !value;
this.cdr.markForCheck();
}
}
constructor(private cdr: ChangeDetectorRef, private host: ElementRef<HTMLElement>) {}
}

function getCustomErrorComponentFactory<C>(
component: Type<C>,
controlErrorComponent: Type<any> = null,
controlErrorComponentAnchorFn: (hostElem: Element, errorElem: Element) => () => void = null
) {
return createComponentFactory({
component,
declarations: [CustomControlErrorComponent],
imports: [
FormsModule,
ReactiveFormsModule,
ErrorTailorModule.forRoot({
errors: {
useValue: {
required: () => 'required error'
}
},
controlErrorComponent: CustomControlErrorComponent,
controlErrorComponentAnchorFn: controlErrorComponentAnchorFn
})
]
});
}

describe('CustomControlErrorComponent', () => {
let spectator: Spectator<CustomErrorFormGroupComponent>;
const createComponent = getCustomErrorComponentFactory(
CustomErrorFormGroupComponent,
CustomControlErrorComponent
);

beforeEach(() => (spectator = createComponent()));

it('should create custom error component', () => {
const input = spectator.query<HTMLInputElement>(byPlaceholder('Name'));

typeInElementAndFocusOut(spectator, '', input);

expect(spectator.query('h1')).toBeTruthy();
expect(spectator.query(byText('required error'))).toBeTruthy();
});
});

describe('ErrorComponentAnchorFnCallback', () => {
let anchorFnCalled = false;
let anchorFnDestroyCalled = false;

let spectator: Spectator<CustomErrorFormGroupComponent>;
const createComponent = getCustomErrorComponentFactory(
CustomErrorFormGroupComponent,
null,
(hostElem: Element, errorElem: Element) => {
anchorFnCalled = true;
expect(hostElem).toBeTruthy();
expect(errorElem).toBeTruthy();
return () => {
anchorFnDestroyCalled = true;
};
}
);

beforeEach(() => (spectator = createComponent()));

it('should call error component anchor callback', () => {
const input = spectator.query<HTMLInputElement>(byPlaceholder('Name'));

typeInElementAndFocusOut(spectator, '', input);

expect(anchorFnCalled).toBeTruthy();
});
});
});
});
20 changes: 14 additions & 6 deletions projects/ngneat/error-tailor/src/lib/control-error.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
EmbeddedViewRef
} from '@angular/core';
import { AbstractControl, ControlContainer, NgControl, ValidationErrors } from '@angular/forms';
import { ControlErrorComponent, IControlErrorComponent } from './control-error.component';
import { DefaultControlErrorComponent, ControlErrorComponent } from './control-error.component';
import { ControlErrorAnchorDirective } from './control-error-anchor.directive';
import { EMPTY, fromEvent, merge, Observable, Subject } from 'rxjs';
import { ErrorTailorConfig, ErrorTailorConfigProvider, FORM_ERRORS } from './providers';
Expand All @@ -33,12 +33,13 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
@Input() controlErrorsOnBlur = true;
@Input() controlErrorAnchor: ControlErrorAnchorDirective;

private ref: ComponentRef<IControlErrorComponent>;
private ref: ComponentRef<ControlErrorComponent>;
private anchor: ViewContainerRef;
private submit$: Observable<Event>;
private control: AbstractControl;
private destroy = new Subject();
private mergedConfig: ErrorTailorConfig = {};
private customAnchorDestroyFn: () => void;

constructor(
private vcr: ViewContainerRef,
Expand Down Expand Up @@ -81,10 +82,10 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {

private setError(text: string, error?: ValidationErrors) {
if (!this.ref) {
const factory = this.resolver.resolveComponentFactory<IControlErrorComponent>(
const factory = this.resolver.resolveComponentFactory<ControlErrorComponent>(
this.mergedConfig.controlErrorComponent
);
this.ref = this.anchor.createComponent<IControlErrorComponent>(factory);
this.ref = this.anchor.createComponent<ControlErrorComponent>(factory);
}
const instance = this.ref.instance;

Expand All @@ -99,12 +100,19 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
}

if (this.mergedConfig.controlErrorComponentAnchorFn) {
this.mergedConfig.controlErrorComponentAnchorFn(this.host, this.ref);
this.customAnchorDestroyFn = this.mergedConfig.controlErrorComponentAnchorFn(
this.host.nativeElement as HTMLElement,
(this.ref.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement
);
}
}

ngOnDestroy() {
this.destroy.next();
if (this.customAnchorDestroyFn) {
this.customAnchorDestroyFn();
this.customAnchorDestroyFn = null;
}
if (this.ref) this.ref.destroy();
this.ref = null;
}
Expand Down Expand Up @@ -142,7 +150,7 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
blurPredicate(element) {
return element.tagName === 'INPUT' || element.tagName === 'SELECT';
},
controlErrorComponent: ControlErrorComponent
controlErrorComponent: DefaultControlErrorComponent
},
...this.config
};
Expand Down
Loading