I am trying to implement a custom validator for a form in Angular. It would be perfect if I could access the this of the controller, but it's undefined in the validator function.
This is my validator function:
validateSuccessShortName(control: AbstractControl) {
//can't read this.randomProp, because this is undefined
if (control.value.length > this.randomProp.length) {
return {value: control.value};
}
return null;
}
Here is a STACKBLITZ demonstrating the issue.
Am I doing something wrong or is this simply impossible in the Angular framework?
https://stackblitz.com/edit/ionic-tqp9o1?embed=1&file=pages/home/home.ts
And for future improvements way better move all validation in another file, and first params for each function must be config. You validators must not dependents from this just from config object
Just change on this.validateSuccessShortName.bind(this), because you function missing context
shortName: new FormControl('', [
this.validateSuccessShortName.bind(this)
])
Your angular validator doesn't reference your component property. To fix this you need to bind this to the validator.
export class HomePage {
private arr: Array<any>;
private thevalue: string;
public thisForm: FormGroup;
public randomProp: string;
constructor(
private modal: ModalController,
public navCtrl: NavController) {
this.thevalue = "Initial value";
this.randomProp = "This is a random property";
this.thisForm = new FormGroup({
shortName: new FormControl('', [
this.validateSuccessShortName.bind(this)
])
});
}
validateSuccessShortName(control: AbstractControl) {
if (control.value.length > this.randomProp.length) {
return {value: control.value};
}
return null;
}
}
Related
Hi I am trying to set/ take away custom validators for different elements in a form array that can change around, so far what I have tried to do is create a switch statement and loop through all of the input types that are set so I could set the validation rule as well as send a message to the user if the rule isn't met. The problem I am having is the form is initialized before the form data is set.
So my question is how can I loop through the array and set the validation rules. If someone could let me know if im along the right tracks with using a switch statement but have code in the wrong place or if there is a different and better approach it would be most helpful thank you
export class ReactiveComponent implements OnInit {
public form: FormGroup;
public fieldList: any;
types: Array<any>;
formData: any;
Param: string;
setData: any;
formLabelNames: any;
get contactFormGroup() {
return this.form.get('inputs') as FormArray;
}
constructor(
private route: ActivatedRoute,
private fb: FormBuilder,
private api: FormService,
private notifiy: NotificationService,
private auth: AuthService,
private router: Router) { }
ngOnInit() {
this.form = this.fb.group({
name: ['', Validators.compose([Validators.required])],
organization: ['', Validators.compose([Validators.required])],
inputs: this.fb.array([this.createForm()])
});
this.route.paramMap.subscribe(params => {
this.Param = params.get('id');
this.getForm(this.Param);
});
// set fieldslist to this field
this.fieldList = this.form.get('inputs') as FormArray;
}
// formgroup
createForm(): FormGroup {
return this.fb.group({
type: ['', Validators.compose([Validators.required])],
name: ['', Validators.compose([Validators.required])],
value: ['', this.validators()]
});
}
getForm(id) {
this.api.getForm(id).subscribe(
(data: any) => this.setForm(data)
);
}
getFieldsFormGroup(index): FormGroup {
const formGroup = this.fieldList.controls[index] as FormGroup;
return formGroup;
}
getContactsFormGroup(index): FormGroup {
const formGroup = this.fieldList.controls[index] as FormGroup;
return formGroup;
}
setForm(data) {
const d = data.results;
this.setData = d;
this.formLabelNames = d[0].fields;
this.form.patchValue({
name: [d[0].form_name],
organization: [d[0].org],
});
this.form.setControl('inputs', this.setExistingFields(d[0].fields));
}
setExistingFields(fields: any): FormArray {
const formArray = new FormArray([]);
this.fieldList = formArray;
fields.forEach(f => {
formArray.push(this.fb.group({
name: f.name,
type: f.type,
value: f.value
}));
});
return formArray;
}
/* This is where I have tried to create a switch statement but I get a undefined error because the setform function is being called after this one */
validators() {
this.formLabelNames.type.forEach((field: any) => {
switch (field.type) {
case 'email':
}
});
}
submit() {
if (this.form.valid) {
const formId = this.Param;
const local = this.auth.decodePayload();
const userId = local.sub;
this.router.navigateByUrl('/dashboard');
this.api.sendForm(this.form.value, formId, userId).subscribe();
this.form.reset();
} else {
this.notifiy.showFailure('Form is not valid', 'Error');
}
}
}
You have several problems, as far as I can judge:
You don't initialize neither the formControls nor the formArray correctly inside of you formGroup. It should rather look like this:
this.form = this.fb.group({
name: new FormControl( "", {validators: [Validators.required]}),
organization: new FormControl( "", {validators: [Validators.required]}),
inputs: this.fb.array([new FormControl(), new FormControl()]),
});
Besides: What is the point of using a formArray when it consists of only one formGroup?
Off course you can set the validators for any abstractControl be it a formControl or a formGroup. For the array it would look like something like this:
this.formArray.controls.map((ctrl) => {
ctrl.setValidators([Validators.required, Validators.email]);
});
It doesn't matter where in the component class you put your method. Though it surely matters when that method is invoked!
I'm new to Angular & TypeScript and trying to figure out how to instantiate an object (before an api request is returned with the real data).
For example, my model looks like this:
//order.model.ts
export class Order {
constructor(public id: number, currency: string, public contact: Object, public items: Array<Object>) {}
}
And then I try to instantiate that in one of my components, let's say the App component:
//app.component.ts
export class AppComponent {
#Input()
public order: Order = new Order();
}
Of course, it expected to receive 4 arguments when instantiating new Order() but received 0. Do I actually have to pass in undefined/empty values for each attribute of Order?
In good ol' React (without TS) I would just initialize with an empty object and call it a day:
this.state = {
order: {}
}
What's best practice for this sort of thing in Angular/TS?
Yes as it is currently set up you would have to pass 4 default arguments to the constructor.
public order: Order = new Order(1, '', {}, []);
Or you can set each property as nullable by adding a ? like so:
export class Order {
constructor(public id?: number, currency?: string, public contact?: Object, public items?: Array<Object>) {}
}
If the class doesn't have functionality (you are simply using it for type checking) the best way to do it would be to declare an interface like so (you can also make them nullable here with ?s):
export interface Order {
id: number;
currency: string;
contact: Object;
items: Object[];
}
then in your component do not initialize the value until you have all of the needed values:
//app.component.ts
export class AppComponent {
#Input()
public order: Order;
// just an example
setValues(id: number, currency: string, contact: Object, items: Object[]) {
this.order = {
id: id,
currency: currency,
contact: contact,
items: items
}
}
// example for if you receive object with correct fields from backend
getData() {
this.service.getData().subscribe(result => {
this.order = result;
});
}
}
Is there a way to create a reactive form basing on an existing data model with all the validation magic. In the example below author passes whole new object into a formbuilder but what I want to achieve is elegant way to tell formbuilder what field is required or needs some other validation.
https://malcoded.com/posts/angular-fundamentals-reactive-forms
export class PersonalData {
email: string = '';
mobile: string = '';
country: string = '';
}
...
createFormGroupWithBuilderAndModel(formBuilder: FormBuilder) {
return formBuilder.group({
personalData: formBuilder.group(new PersonalData()),
requestType: '',
text: ''
});
}
I just want to skip the process of assigning a FormControl for each field in the model.
#EDIT
After some research and little hint from #xrobert35 I wanted to try and used https://www.npmjs.com/package/#rxweb/reactive-form-validators
They could be "many" way to do what you want to do, but by just extending your actual solution : Your personnal data could look like :
export class PersonalData {
email: string = ['', Validators.required];
mobile: string = ['', Validators.required, Validators.maxLength(10)];
country: string = '';
}
If you need domain base validation (for reusable purpose) you can use rxweb validations.
export class PersonalData {
#required()
email: string;
#required()
mobile: string;
#greaterThan({ fieldName: 'number2' })
number1: number;
#prop()
number2: number;
}
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
formGroup: FormGroup;
constructor( private validation: RxFormBuilder) {
}
ngOnInit() {
let person: PersonalData = new PersonalData();
this.formGroup = this.validation.formGroup(person);
}
}
If I understand you just want to add validators to your field.
https://angular.io/guide/form-validation
I can't be more precise to giving you the official documentation.
ngOnInit(): void {
this.heroForm = new FormGroup({
'name': new FormControl(this.hero.name, [
Validators.required,
Validators.minLength(4),
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
]),
'alterEgo': new FormControl(this.hero.alterEgo),
'power': new FormControl(this.hero.power, Validators.required)
});
}
In this exemple the fields name and power are required and of course the syntaxe could differ but the flow is the same.
Does it helps you ?
#EDIT
There is the same post for your use case:
https://stackoverflow.com/a/47576916/7133262
I get my data from http with rjsx in component (let name it customer).
Then i'm using inner component in customer:
<customer>
<customer-form [customer]="customer"></customer-form>
</customer>
<!-- [customer]="customer" // here is data from http -->
and in customer-form i have:
#Input() customer:ICustomer;
complexForm : FormGroup;
constructor(fb: FormBuilder) {
this.complexForm = fb.group({
'name': [this.customer['name'], Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(255)])]
});
}
but i get:
Cannot read property 'name' of undefined
TypeError: Cannot read property 'name' of undefined
if i understood correctly: it's due to the fact that constructor is called, but data isn't fetched yet from http, so customer is empty. But how to fix this?
upd: my http data get:
getCustomer(id) {
this.customerService.getCustomer(id)
.subscribe(
customer => this.customer = customer,
error => this.errorMessage = <any>error);
}
----
#Injectable()
export class CustomerService {
private customersUrl = 'api/customer';
constructor (private http: Http) {}
getCustomers (): Observable<ICustomer[]> {
return this.http.get(this.customersUrl)
.map(this.extractData)
.catch(this.handleError);
}
getCustomer (id): Observable<ICustomer> {
return this.http.get(this.customersUrl + '/' + id)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body || { };
}
private handleError (error: Response | any) {
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
as #Bhushan Gadekar stated, you are accessing customer when it has not been initialized.
There are multiple way to handle this correctly :
Using a setter:
#Input("customer")
set _customer(c:ICustomer){
this.customer=c;
this.complexForm.get("name").setValue(c.name,{onlySelf:true});
}
customer:ICustomer;
complexForm : FormGroup;
constructor(fb: FormBuilder) {
this.complexForm = fb.group({
'name': [null, Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(255)])]
});
}
Using an Observable
Here, the customer needs to be an Observable of ICustomer
#Input() customer:Observable<ICustomer>;
complexForm : FormGroup;
constructor(fb: FormBuilder) {
this.complexForm = fb.group({
'name': [this.customer['name'], Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(255)])]
});
}
ngOnInit(){
this.customer.map(c=>this.complexForm.get("name").setValue(c.name,{onlySelf:true}))
.subscribe();
}
Mixing both :
#Input("customer")
set _customer(c:ICustomer){
this.customer.next(c);
}
customer=New Subject<ICustomer>();
complexForm : FormGroup;
constructor(fb: FormBuilder) {
this.complexForm = fb.group({
'name': [null, Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(255)])]
});
}
ngOnInit(){
this.customer.map(c=>this.complexForm.get("name").setValue(c.name,{onlySelf:true}))
.subscribe();
}
Case for multiple properties :
If you don't want to write every form update one by one, and if your form's field names are the same as your Object you can loop over customer properties:
Object.keys(customer).forEach(k=>{
let control = this.complexForm.get(k);
if(control)
control.setValue(customer[k],{onlySelf:true});
});
Note that this code will work only if your form's controls are named the same way as customer's properties are. If not, you may need to make a hash mapping customer properties name to formControls name.
Important point:
Yous should never access inputs from the constructor as they are not populated yet, all inputs should get populated (at least the synchronous ones) just before the ngOnInit hook. Take a look at the Lifecycle hooks documentation
I can see that you are trying to access customer object when it is not populated.
Issue here is that http call takes some time to be resolved.thus, your view is trying to access customer object even when it is undefined.
try this:
<customer *ngIf="customer">
<customer-form [customer]="customer"></customer-form>
</customer>
Though the way you are accessing name property is also not good.
Best approach is to create a customer model and use your property as className.propertyName
Hoe this helps.
Instead of ngOnInit , try ngAfterViewInit
do not use subscribe in component.ts and add async pipe in component.html, like so:
<customer-form [customer]="customer | async"></customer-form>
I'm new to angular2 so I will try to make this question as clear as possible. I want to have a method in my model that I can call from my service. Right now I have it trying to replace the name.
Here is my model
export class Secret {
public name: string;
constructor (
public id: number,
public type: string,
public visible_data: any,
public secrets?: any,
public group_id?: number,
public group_name?: string
) {
this.name = this.myName();
}
public myName(): string {
return this.name = "whaddup"
}
}
and my service method
/*
* get secrets
*/
public getSecrets(): Promise<Secret[]> {
let tempArray = [];
return this.sdk.list_secrets()
.then((resp) => {
resp.map((item) => {
tempArray.push({
id: item.id,
name: this.secret.myName(),
type: item.type,
visible_data: {
"username": item.data
}
}); // end push
});
return tempArray;
})
.catch((err) => {
console.error(err);
});
}
list.component.ts code:
export class ListComponent implements OnInit {
public constantArray: Secret[];
private secrets: Secret[];
private secret: Secret;
constructor(private secretService: SecretService) { }
public ngOnInit() {
this.getSecrets();
}
public getSecrets() {
this.secretService.getSecrets()
.then((data) => {
this.secrets = data;
this.constantArray = data;
});
}
}
Instead of
tempArray.push({
id: item.id,
name: this.secret.myName(),
type: item.type,
visible_data: {
"username": item.data
}); // end push
You should do
tempArray.push(new Secret(item.id, ...));
Reason: in your original code, the object pushed to array are plain old javascript object and they are not typescript objects. The 'new ...' will create a real typescript class object.
To work with angular2 service, you need to do two things. First of all you need to at the #Injectable annotation on top of your services.
If you want to inject services into each other, you need to provide them on your NgModule like this:
#NgModule({
selector: 'my-app',
providers: [NameService]
}):
If you want to inject the service into another service/component you can just leverage typescript and use constructor injection like this:
constructor(private secretService: SecretService) {}