Skip to content

Commit

Permalink
feat: ✨ update validate function to suport bootstrap
Browse files Browse the repository at this point in the history
✨ update validate function to support bootstrap

BREAKING CHANGE: add bootstrap validation message, change validator target selector
  • Loading branch information
Hung Pham committed May 16, 2023
1 parent b82c355 commit db22ec7
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FormatedError } from '../models';

export abstract class BaseValidationMessagesComponent {
errors: FormatedError[] = [];
classes = '';

trackByFn: TrackByFunction<FormatedError> = (_, item) => item.key;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { ChangeDetectionStrategy, Component, HostBinding, ViewEncapsulation } from '@angular/core';
import { BaseValidationMessagesComponent } from '../base-validation-messages';

@Component({
Expand All @@ -9,7 +9,8 @@ import { BaseValidationMessagesComponent } from '../base-validation-messages';
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
encapsulation: ViewEncapsulation.None
})
export class DefaultValidationMessagesComponent extends BaseValidationMessagesComponent {
@HostBinding('class') override classes: string = '';
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
FormEventType,
FormValidatorConfig,
FormatedError,
UIFramework,
} from '../models';
import {
BaseValidationMessagesComponent,
Expand Down Expand Up @@ -96,11 +97,16 @@ export class FormControlValidatorDirective implements AfterViewInit, OnDestroy {

private _subscriptions = new Subscription();
private _errorRef:
| ComponentRef<BaseValidationMessagesComponent>
| EmbeddedViewRef<any>
| null = null;
| ComponentRef<BaseValidationMessagesComponent>
| EmbeddedViewRef<any>
| null = null;

private _events$ = new Subject<FormEvent>();
private _cachedValidationErrors = '';

private get hasCacheValidationErrors(): boolean {
return Boolean(this._cachedValidationErrors);
}

constructor(
private readonly changeDetectorRef: ChangeDetectorRef,
Expand All @@ -123,15 +129,16 @@ export class FormControlValidatorDirective implements AfterViewInit, OnDestroy {
private readonly config: FormValidatorConfig,

@Optional()
public readonly parentFormGroupValidatorDirective: FormGroupValidatorDirective
) {}
public readonly parentFormGroupValidatorDirective: FormGroupValidatorDirective,
) { }

ngAfterViewInit(): void {
this.listenFormEvents();
}

ngOnDestroy(): void {
this._subscriptions.unsubscribe();
this.removeValidationErrors();
}

@HostListener('blur', ['$event'])
Expand All @@ -144,31 +151,19 @@ export class FormControlValidatorDirective implements AfterViewInit, OnDestroy {
return;
}

let _cachedErrors = '';

const sub = merge(this.parent.events$, this._events$)
.pipe(
skipWhile(() => this.skipValidate),
map(() => this.formatErrors())
)
.subscribe((errors) => {
if (_cachedErrors === JSON.stringify(errors)) {
return;
}

this.removeValidationErrors();
if (this.shouldShowValidate(errors)) {
_cachedErrors = JSON.stringify(errors);
this.showValidationErrors(errors);
} else {
_cachedErrors = '';
}
this.validate(errors);
});

this._subscriptions.add(sub);
}

private shouldShowValidate(errors: FormatedError[]): boolean {
private shouldShowValidationError(errors: FormatedError[]): boolean {
if (errors.length <= 0) {
return false;
}
Expand Down Expand Up @@ -227,8 +222,50 @@ export class FormControlValidatorDirective implements AfterViewInit, OnDestroy {
}
}

private showValidationErrors(errors: FormatedError[]): void {
const viewContainerRef = this.getViewContainerRef();
private cacheValidationErrors(errors: FormatedError[]): void {
this._cachedValidationErrors = JSON.stringify(errors);
}

private clearCacheValidationErrors(): void {
this._cachedValidationErrors = '';
}

private isValidationErrorsChanged(errors: FormatedError[]): boolean {
return this._cachedValidationErrors !== JSON.stringify(errors);
}

private validate(errors: FormatedError[]): void {
if (!this.isValidationErrorsChanged(errors)) {
return;
}

if (this.config.uiFrameWork === 'auto') {
console.log('Validation');
}

const uiFrameWork = this.config.uiFrameWork === 'auto' ? this.detectUIFramework() : this.config.uiFrameWork;

switch (uiFrameWork) {
case UIFramework.Bootstrap:
this.bootstrapValidate(errors);
break;

default:
this.defaultValidate(errors);
break;
}
}

private getViewContainerRef(): ViewContainerRef {
const targetRef = this.containerRef
? this.containerRef.targetRef
: this.targetRef;
return targetRef ? targetRef.viewContainerRef : this.viewContainerRef;
}

private defaultValidate(errors: FormatedError[]): void {
const targetRef = this.containerRef ? this.containerRef.targetRef : this.targetRef;
const viewContainerRef = targetRef ? targetRef.viewContainerRef : this.viewContainerRef;

if (
this.validationMessageTemplateRef &&
Expand All @@ -242,7 +279,9 @@ export class FormControlValidatorDirective implements AfterViewInit, OnDestroy {
} else if (this.validationMessageComponent) {
this._errorRef = viewContainerRef.createComponent(
this.validationMessageComponent,
{ index: viewContainerRef.length }
{
index: viewContainerRef.length
}
);

if (this._errorRef instanceof ComponentRef && this._errorRef.instance) {
Expand All @@ -252,10 +291,82 @@ export class FormControlValidatorDirective implements AfterViewInit, OnDestroy {
}
}

private getViewContainerRef(): ViewContainerRef {
const targetRef = this.containerRef
? this.containerRef.targetRef
: this.targetRef;
return targetRef ? targetRef.viewContainerRef : this.viewContainerRef;
private bootstrapValidate(errors: FormatedError[]): void {
this.removeValidationErrors();

if (this.shouldShowValidationError(errors)) {
if (!this.hasCacheValidationErrors) {
this.bootstrapAddErrorClassToControl();
}
this.bootstrapShowValidationError(errors);
this.cacheValidationErrors(errors);
} else {
this.clearCacheValidationErrors();
this.bootstrapRemoveErrorClassFromControl();
}
}

private bootstrapAddErrorClassToControl(): void {
(this.elementRef.nativeElement as HTMLElement).classList.add('is-invalid');
}

private bootstrapRemoveErrorClassFromControl(): void {
(this.elementRef.nativeElement as HTMLElement).classList.remove('is-invalid');
}

private bootstrapShowValidationError(errors: FormatedError[]): void {
const targetRef = this.containerRef ? this.containerRef.targetRef : this.targetRef;
const viewContainerRef = targetRef ? targetRef.viewContainerRef : this.viewContainerRef;

if (
this.validationMessageTemplateRef &&
this.validationMessageTemplateRef instanceof TemplateRef
) {
this._errorRef = viewContainerRef.createEmbeddedView(
this.validationMessageTemplateRef,
{ $implicit: errors },
viewContainerRef.length
);
} else if (this.validationMessageComponent) {
this._errorRef = viewContainerRef.createComponent(
this.validationMessageComponent,
{
index: viewContainerRef.length
}
);

if (this._errorRef instanceof ComponentRef && this._errorRef.instance) {
this._errorRef.instance.errors = errors;
this._errorRef.instance.classes = 'invalid-feedback';
this.changeDetectorRef.detectChanges();
}
}

if (!targetRef) {
let rootNode: HTMLElement | null = null;
if (this._errorRef instanceof ComponentRef) {
rootNode = (this._errorRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
} else if (this._errorRef instanceof EmbeddedViewRef) {
rootNode = this._errorRef.rootNodes[0] as HTMLElement;
}

const containerTarget = (viewContainerRef.element.nativeElement as HTMLElement).parentElement;
if (rootNode && containerTarget) {
containerTarget.appendChild(rootNode);
}
}
}

private detectUIFramework(): UIFramework {
const classList = (this.elementRef.nativeElement as HTMLElement).classList;
if (classList.contains('form-control') || classList.contains('form-select') || classList.contains('form-check-input')) {
return UIFramework.Bootstrap
}

if (classList.contains('mat-input-element') || classList.contains('mat-select')) {
return UIFramework.AngularMaterial
}

return UIFramework.None;
}
}
2 changes: 2 additions & 0 deletions projects/ngx-validator/src/lib/directives/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './form-control-validator.directive';
export * from './form-group-validator.directive';
export * from './validator-container.directive';
export * from './validator-target.directive';
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
selector: '[validatorTarget]',
exportAs: 'validatorTarget',
selector: '[validatorMessagesTarget]',
exportAs: 'validatorMessagesTarget',
})
export class ValidatorTargetDirective {
constructor(public viewContainerRef: ViewContainerRef) {}
Expand Down
16 changes: 13 additions & 3 deletions projects/ngx-validator/src/lib/form-validator.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { FormValidatorConfig } from './models';
import { FormValidatorConfig, UIFramework } from './models';
import { FORM_VALIDATOR_CONFIGURATION } from './form-validator-token';
import { DefaultValidationMessagesComponent } from './components';
import { FormGroupValidatorDirective, FormControlValidatorDirective } from './directives';
import {
FormGroupValidatorDirective,
FormControlValidatorDirective,
ValidatorContainerDirective,
ValidatorTargetDirective
} from './directives';

export const defaultFormValidationConfig: FormValidatorConfig = {
skipValidate: false,
uiFrameWork: UIFramework.Auto,
validateOn: ({ dirty, touched, submited }) => {
return (dirty && touched) || submited;
},
Expand All @@ -30,6 +36,8 @@ export const defaultFormValidationConfig: FormValidatorConfig = {
declarations: [
FormGroupValidatorDirective,
FormControlValidatorDirective,
ValidatorContainerDirective,
ValidatorTargetDirective,
DefaultValidationMessagesComponent,
],
providers: [
Expand All @@ -40,7 +48,9 @@ export const defaultFormValidationConfig: FormValidatorConfig = {
],
exports: [
FormGroupValidatorDirective,
FormControlValidatorDirective
FormControlValidatorDirective,
ValidatorContainerDirective,
ValidatorTargetDirective,
],
})
export class FormValidatorModule {
Expand Down
8 changes: 8 additions & 0 deletions projects/ngx-validator/src/lib/models/form-validator.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ export interface FormEvent {
value?: any;
}

export const enum UIFramework {
Bootstrap = 'bootstrap',
AngularMaterial = 'angular-material',
Auto = 'auto',
None = 'none',
}

export interface FormValidatorConfig {
skipValidate?: boolean;
uiFrameWork?: UIFramework | 'default' | 'auto';
defaultErrorMessage?: ErrorMessage;
unknownErrorMessage?: string;
validationMessagesComponent?: Type<BaseValidationMessagesComponent>;
Expand Down

0 comments on commit db22ec7

Please sign in to comment.