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
Next Next commit
feat(config): custom error component & positioning
  • Loading branch information
harikvpy committed Jun 26, 2020
commit fc90011bfe98c5c03c9b86c0b283541955ea6556
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ The library adds a `form-submitted` to the submitted form. You can use it to sty

## Config

### Global Config
- `blurPredicate` - Elements that should listen the `focusout` event. The default predicate is:

```ts
Expand All @@ -198,7 +199,47 @@ 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`.

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

`CustomControlErrorComponent` is declared as:
```ts
@Component(...)
export class CustomControlErrorComponent implements IControlErrorComponent {

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

set text(value: string|null) {
this._text = value;
this.hide = !value;
this.cdr.markForCheck();
}
}
```
- `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.

In the following example, the error component is attached to the form field's
grandparent element.
```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);
}
```

### Per Control Config
- `controlErrorsOnBlur` - To modify the error display behavior and show the errors on submission alone, set the following input:

```html
Expand Down
6 changes: 5 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, TemplateRef } from '@angular/core';
import { ValidationErrors } from '@angular/forms';

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

@Component({
selector: 'control-error',
template: `
Expand All @@ -20,7 +26,7 @@ import { ValidationErrors } from '@angular/forms';
`
]
})
export class ControlErrorComponent {
export class ControlErrorComponent implements IControlErrorComponent {
_text: string | null = null;
_tpl: TemplateRef<{ $implicit: ValidationErrors; text: string }> | undefined;
context: { $implicit: ValidationErrors; text: string };
Expand Down
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 @@ -10,10 +10,11 @@ import {
Optional,
Self,
TemplateRef,
ViewContainerRef
ViewContainerRef,
EmbeddedViewRef
} from '@angular/core';
import { AbstractControl, ControlContainer, NgControl, ValidationErrors } from '@angular/forms';
import { ControlErrorComponent } from './control-error.component';
import { ControlErrorComponent, IControlErrorComponent } 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 @@ -32,7 +33,7 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
@Input() controlErrorsOnBlur = true;
@Input() controlErrorAnchor: ControlErrorAnchorDirective;

private ref: ComponentRef<ControlErrorComponent>;
private ref: ComponentRef<IControlErrorComponent>;
private anchor: ViewContainerRef;
private submit$: Observable<Event>;
private control: AbstractControl;
Expand Down Expand Up @@ -80,8 +81,10 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {

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

Expand All @@ -94,6 +97,10 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
if (this.controlErrorsClass) {
instance.customClass = this.controlErrorsClass;
}

if (this.mergedConfig.controlErrorComponentAnchorFn) {
this.mergedConfig.controlErrorComponentAnchorFn(this.host, this.ref);
}
}

ngOnDestroy() {
Expand Down Expand Up @@ -134,7 +141,8 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
...{
blurPredicate(element) {
return element.tagName === 'INPUT' || element.tagName === 'SELECT';
}
},
controlErrorComponent: ControlErrorComponent
},
...this.config
};
Expand Down
5 changes: 4 additions & 1 deletion projects/ngneat/error-tailor/src/lib/providers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InjectionToken, ValueSansProvider, FactorySansProvider } from '@angular/core';
import { InjectionToken, ValueSansProvider, FactorySansProvider, ElementRef, ComponentRef, Type } from '@angular/core';
import { ErrorsMap } from './types';
import { IControlErrorComponent } from './control-error.component';

export const FORM_ERRORS = new InjectionToken('FORM_ERRORS', {
providedIn: 'root',
Expand All @@ -21,6 +22,8 @@ export type ErrorsProvider = ErrorsUseValue | ErrorsUseFactory;
export type ErrorTailorConfig = {
errors?: ErrorsProvider;
blurPredicate?: (element: Element) => boolean;
controlErrorComponent?: Type<IControlErrorComponent>;
controlErrorComponentAnchorFn?: (host: ElementRef, errorComponent: ComponentRef<IControlErrorComponent>) => void;
};

export const ErrorTailorConfigProvider = new InjectionToken<ErrorTailorConfig>('ErrorTailorConfigProvider');
20 changes: 18 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgModule, ElementRef, ComponentRef, EmbeddedViewRef } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ErrorTailorModule } from '@ngneat/error-tailor';
import { ErrorTailorModule, IControlErrorComponent } from '@ngneat/error-tailor';
import { CommonModule } from '@angular/common';
import { CustomErrorControlModule } from './custom-error-control/custom-error-control.module';
import { CustomControlErrorComponent } from './custom-error-control/custom-error-control.component';

/**
* Hook function to attach error messages to the control's grandparent rather than its parent.
* Uses direct manipulation of DOM.
*/
function 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);
}

@NgModule({
declarations: [AppComponent],
imports: [
CommonModule,
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
CustomErrorControlModule,
ErrorTailorModule.forRoot({
errors: {
useFactory() {
Expand All @@ -24,6 +38,8 @@ import { ErrorTailorModule } from '@ngneat/error-tailor';
},
deps: []
}
//controlErrorComponent: CustomControlErrorComponent, // Uncomment to see errors being rendered using a custom component
//controlErrorComponentAnchorFn: controlErrorComponentAnchorFn // Uncomment to see errors being positioned differently
})
],
providers: [],
Expand Down
51 changes: 51 additions & 0 deletions src/app/custom-error-control/custom-error-control.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, TemplateRef } from '@angular/core';
import { IControlErrorComponent } from '@ngneat/error-tailor';
import { ValidationErrors } from '@angular/forms';

@Component({
selector: 'custom-control-error',
template: `
<div class="control-error" [class.hide-control]="hide" *ngIf="!_tpl">
<h3>{{ _text }}</h3>
</div>
<ng-template *ngTemplateOutlet="_tpl; context: context"></ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
.hide-control {
display: none !important;
}

:host {
display: block;
}
`
]
})
export class CustomControlErrorComponent implements IControlErrorComponent {
_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) {
this._tpl = tpl;
this.context = { $implicit: error, text };
this.cdr.markForCheck();
}

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>) {}
}
10 changes: 10 additions & 0 deletions src/app/custom-error-control/custom-error-control.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomControlErrorComponent } from './custom-error-control.component';

@NgModule({
declarations: [CustomControlErrorComponent],
imports: [CommonModule],
exports: [CustomControlErrorComponent]
})
export class CustomErrorControlModule {}