Cannot get Angular 2 custom validator working using cookbook approach.
My main goal is to learn Angular (that is, I am noob, as always).
I am trying to have an input text box that only allows integers in a specific range (1-26). I thought that I would die and write a validator that accepts an arbitrary list of numbers and ranges (for example, "1,3,5,7,11-19,100-200") and checks that this value is one of the valid values.
My problem is that a custom validator (anonymous function is returned allowedNumericValuesValidator?) Is never called. (But keep in mind that the built validator requiredworks fine.) For and this is not one of the methods defined in
AllowedNumericValuesDirective. The validator source itself loads the code, but as for things.
Using Angular 2.2.3, angular-cli 1.0.0-beta.22-1. Chrome browser 55.0.2883.95 (64-bit)
The source is at https://github.com/JohnL4/Diaspora , but I will try to put the relevant parts here.
Here is what I did:
My validator is as follows:
import { Directive, Input, OnChanges, SimpleChanges } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn, Validators } from '@angular/forms';
const SELECTOR: string = 'allowedNumericValues';
class Range
{
constructor ( public Low: number, public High: number) {}
}
export function allowedNumericValuesValidator( anAllowedValuesSpec: string): ValidatorFn
{
let errors: string[] = [];
let ranges : Range[] = [];
let rangeSpecs = anAllowedValuesSpec.split( /\s*,\s*/);
for (let r of rangeSpecs)
{
let ends : string[] = r.split( /\s*-\s*/);
if (ends.length == 1)
{
let end : number = Number(ends[0]);
if (isNaN( end))
errors.push( r + " is NaN");
else
ranges.push( new Range( end, end));
}
else if (ends.length == 2)
{
let low:number = Number(ends[0]);
let high:number = Number(ends[1]);
if (isNaN( low) || isNaN( high))
errors.push( r + " has NaN");
else
ranges.push( new Range( low, high));
}
else
errors.push( r + " has bad syntax");
}
if (errors.length > 0)
throw new Error( errors.join( "; "));
return (control: AbstractControl): {[key: string]: any} => {
const numberToBeValidated = control.value;
const num = Number( numberToBeValidated);
if (isNaN( num))
return {SELECTOR: {numberToBeValidated}};
let isGood: boolean = false;
for (let r of ranges)
{
if (r.Low <= num && num <= r.High)
{
isGood = true;
break;
}
}
return isGood ? null : {SELECTOR: {numberToBeValidated}};
};
}
@Directive({
selector: '[' + SELECTOR + ']',
providers: [{provide: NG_VALIDATORS, useExisting: AllowedNumericValuesDirective, multi: true}]
})
export class AllowedNumericValuesDirective implements Validator, OnChanges
{
@Input() allowedNumericValues: string;
private valFn = Validators.nullValidator;
ngOnChanges( changes: SimpleChanges): void
{
const change = changes[ SELECTOR];
if (change)
{
const val: string = change.currentValue;
this.valFn = allowedNumericValuesValidator( val);
}
else
this.valFn = Validators.nullValidator;
}
validate( control: AbstractControl): {[key: string]: any}
{
return this.valFn( control);
}
}
const SELECTOR,
(callstack - __webpack_require__),
( ,
console.log(), .
My shared.module.ts, shared,
, :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedComponent } from './shared.component';
import { AllowedNumericValuesDirective } from './allowed-numeric-values.directive';
@NgModule({
imports: [
CommonModule
],
declarations: [SharedComponent, AllowedNumericValuesDirective]
})
export class SharedModule { }
app.module.ts ( 4 ,
"", ):
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';
import { SharedModule } from './shared/shared.module';
import { AppComponent } from './app.component';
import { ClusterDetailsComponent } from './cluster-details/cluster-details.component';
import { DotComponent } from './dot/dot.component';
import { GeneratorParamsComponent } from './generator-params/generator-params.component';
import { TabsComponent } from './tabs/tabs.component';
import { XmlComponent } from './xml/xml.component';
@NgModule({
declarations: [
AppComponent,
ClusterDetailsComponent,
DotComponent,
GeneratorParamsComponent,
TabsComponent,
XmlComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
SharedModule,
RouterModule.forRoot([
{
path: '',
redirectTo: '/params',
pathMatch: 'full'
},
{
path: 'params',
component: GeneratorParamsComponent
},
{
path: 'details',
component: ClusterDetailsComponent
},
{
path: 'xml',
component: XmlComponent
},
{
path: 'dot',
component: DotComponent
}
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
generator-params.component.html :
<p></p>
<form #parmsForm="ngForm" class="form-horizontal">
<div class="form-group">
<label for="numSystems" class="col-sm-3 control-label">
Number of systems in cluster
</label>
<div class="col-sm-2">
<input id="numSystems" name="numSystems" type="text" class="form-control"
required maxlength="2" allowedNumericValues="1-26"
[(ngModel)]="numSystems">
</div>
<div *ngIf="formErrors.numSystems" class="col-sm-6 alert alert-danger">
{{ formErrors.numSystems }}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<div class="checkbox">
<label for="slipstreamsHighLow">
<input id="slipstreamsHighLow" name="slipstreamsHighLow" type="checkbox" />
Slipstreams Differentiated Between High & Low Slipknots
</label>
</div>
</div></div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button id="goBtn" (click)="generateCluster()" class="btn btn-default btn-warning"
title="Obviously, this will hammer your existing cluster. Be sure you have it backed up or otherwise saved, or that you don't care."
>
Go!
</button>
<button id="revertBtn" class="btn btn-default" (click)="revertParams()">Revert</button>
</div>
</div>
</form>
, , generator-params.component.ts :
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { Cluster } from '../cluster';
@Component({
selector: 'app-generator-params',
templateUrl: './generator-params.component.html',
styleUrls: ['./generator-params.component.css']
})
export class GeneratorParamsComponent implements OnInit {
private numSystems: string;
parmsForm: NgForm;
@ViewChild( 'parmsForm') currentForm: NgForm;
formErrors = {
'numSystems': ''
};
validationMessages = {
'numSystems': {
'required': "A number of systems is required",
'allowedNumericValues': "Value must be one of the allowed numeric values"
}
};
private _cluster: Cluster;
constructor(aCluster: Cluster)
{
this._cluster = aCluster;
if (aCluster && aCluster.numSystems)
this.numSystems = aCluster.numSystems.toString();
}
ngOnInit()
{
}
ngAfterViewChecked()
{
this.formChanged();
}
public generateCluster()
{
this._cluster.numSystems = Number( this.numSystems);
}
public revertParams()
{
this.numSystems = this._cluster.numSystems.toString();
}
formChanged()
{
if (this.currentForm === this.parmsForm) return;
this.parmsForm = this.currentForm;
if (this.parmsForm)
this.parmsForm.valueChanges.subscribe( data => this.onValueChanged( data));
}
onValueChanged( data?: any)
{
if (!this.parmsForm) return;
const form = this.parmsForm.form;
for (const field in this.formErrors)
{
this.formErrors[field] = '';
const control = form.get( field);
if (control && control.dirty && !control.valid)
{
const messages = this.validationMessages[field];
for (const key in control.errors)
{
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
}
, ,
- ,
.
- , ?
.