bind form validation to a component outside the current component - javascript

i create a searchable-dropdown components and i need to use it in many components , now i have a problem for bind vlaidation of form to this components .
for example i have a from for create a user and i need bind validation of one field to searchable-dropdown components .
private createForm(): void {
this.courseAddForm = this.formBuilder.group({
name: ['', [
Validators.required,
Validators.maxLength(this.val.maxLen.title)
]],
roleId: ['', Validators.compose([Validators.required])]
});
}
i need to bind roleId Validation in this components :
<div class="col-lg-6 kt-margin-bottom-20-mobile">
<kt-searchable-dropdown [formGroup]="courseAddForm" [formcontrolName]="'roleId'"
(selectedId)="selectedCourse($event)" [formTitle]="'COURSE.COURSE_GROUP'">
</kt-searchable-dropdown>
</div>
i try this code for find vlaidation of this form for roleId but its not work for me :
#Input() url: string;
#Input() formTitle: string;
#Input() ItemId: number;
#Input() formcontrolName: string;
#Input() formGroup: FormGroup;
#Input() control: FormControl;
#Output() selectedId = new EventEmitter<number>();
fieldErrors(field: string): any {
let controlState = this.formGroup.controls[field];
return (controlState.dirty || controlState.touched) ? controlState.errors : null;
}
HTML :
<div class="col-lg-12 mt-4 kt-margin-bottom-20-mobile">
<mat-form-field class="mat-form-field-fluid" appearance="outline">
<mat-label>{{'GENERAL.TITLE' | translate}} *</mat-label>
<input [formControlName]="formcontrolName" (keyup)="getValues($event.target.value)" matInput
[placeholder]="'GENERAL.TITLE' | translate">
<span *ngIf="fieldErrors(formcontrolName)" class="text-right">
<label *ngIf="fieldErrors(formcontrolName).required">WORKED</label>
</span>
</mat-form-field>
</div>
How can i Solve this Problem ????

You need to implement a CustomValueAccessor inside this searchable-dropdown component.
For example, a custom file component able to be used on reactive forms:
#Component({
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: FileUploadComponent,
multi: true
}
]
})
export class FileUploadComponent implements ControlValueAccessor {
#Input() progress;
onChange: Function;
private file: File | null = null;
#HostListener('change', ['$event.target.files']) emitFiles( event: FileList ) {
const file = event && event.item(0);
this.onChange(file);
this.file = file;
}
constructor( private host: ElementRef<HTMLInputElement> ) {
}
writeValue( value: null ) {
// clear file input
this.host.nativeElement.value = '';
this.file = null;
}
registerOnChange( fn: Function ) {
this.onChange = fn;
}
registerOnTouched( fn: Function ) {
}
}
Here is a detailed blog post about what you need to do.

Related

Display an array of data from function in Angular

My goal is to display a cross or a check according to the vote.result data from the polls.
I had to use Angular only few times and I feel pretty lost honestly.
TS file (angular) :
#Component({
selector: 'app-deck-card',
templateUrl: './deck-card.component.html',
styleUrls: ['./deck-card.component.scss'],
})
export class DeckCardComponent implements OnInit {
#Input() isAnim: boolean;
#Input() inGame: boolean;
#Input() editMode: boolean;
#Input() readOnly: boolean;
#Input() deckIsBase: boolean;
#Input() card: CardDto;
#Input() polls: PollDto[];
#Input() isSearch: boolean;
#Input() isImport: boolean;
#Input() idDeck: number;
#Input() editRight: boolean;
#Output() changeVote = new EventEmitter<number>();
#Output() deleteEvent = new EventEmitter<number>();
#Output() duplicateEvent = new EventEmitter<CardDto>();
#Output() importEvent = new EventEmitter<CardDto>();
#Output() sharedToCommunityEvent = new EventEmitter<CardDto>();
safeIcon: SafeUrl | string;
votes: VoteDto[];
constructor(private readonly authState: AuthState,
private sanitizer: DomSanitizer) {
}
ngOnInit(): void {
this.safeIcon = this.sanitizer.bypassSecurityTrustUrl(this.card?.theme?.icon);
this.votes = this.polls?.find(p => p.card.id === this.card?.id)?.votes;
}
/**
* Emit the card ID to delete the card
* #return void
*/
deleteCard(): void {
this.deleteEvent.emit(this.card.id);
}
showTheResult(): string {
console.log(this.polls);
console.log(this.votes);
this.polls?.forEach(vote => {
if (vote.voted && vote.result == false) {
// display a mat-icon cross
console.log(vote)
return '<mat-icon>clear</mat-icon>'
} else if (vote.voted && vote.result == true) {
// display a mat-icon check
console.log(vote)
return '<mat-icon>done</mat-icon>'
}
});
return '';
}
}
My 2 console.log in showTheResult() are always undefined.
So, obviously, the console log in the if condition are never reached.
HTML file :
<div class="card-body" [class.card-body-readOnly]="readOnly">
<p class="main-text" [class.readOnly]="readOnly" [class.short]="inGame && isAnim"
[class.long]="!editMode && !isAnim">{{card?.text}}</p>
<p>{{showTheResult()}}</p>
<p>DISPLAY HERE THE MAT-ICON</p>
<span *ngIf="isAnim || editMode" class="sub-text">#{{card?.id}}</span>
</div>
can someone show me the way ?
The DTOs look like this:
export interface PollDto {
id: number;
result: boolean;
voted: boolean;
priority: number;
card: CardDto;
votes: VoteDto[];
}
export interface VoteDto {
participantId: number;
participantName?: string;
pollId: number;
result: boolean;
}
since your this.polls is an #Input(), you don't know if this variable is actually loaded when you reach ngOnInit lifecycle.
When working with #Input data, if you want to catch the moment data is loaded, you should watch the changes :
https://ultimatecourses.com/blog/detect-input-property-changes-ngonchanges-setters
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
}
This way, you will see if ever your data are loaded, if not, that means the problem is in the parent container component.
Also, a quick note : I don't think you should return HTML in your
method, you probably want to handle this another way, with a directive
or something, this would not be a good practice.
Cheers ! :)

Angular 11 how to extend a Form from a base component using a child component

My app has many Fees. All Fees share the same attributes except a few here and there. Say I have this structure:
// Base Fee
interface IFee {
id: string;
name: string;
price: string;
date: string;
}
interface IWebFee extends IFee {
url: string;
links: number;
}
interface IBookFee extends IFee {
pageCount: number;
bookTitle: string;
}
So lets say I wanted to create a Form to edit a BookFee. Content projection wont work since there wont be any context. So I tried creating an embedded view... but I still cant access the parent FormGroup to append controls to it. Here is what I have (which throws an error for missing control because I cant access the FormGroup from the BaseFeeFormComponent):
base-fee-form.component.ts
#Component({
selector: 'app-base-fee-form',
...
providers: [
{
provide: ControlContainer,
useFactory: (comp: BaseFeeFormComponent) => comp.ngForm,
deps: [BaseFeeFormComponent],
},
],
})
export class BaseFeeFormComponent implements AfterContentInit {
#ContentChild('feeChild') templateChild: TemplateRef<any>;
#ViewChild('mountRef', { read: ViewContainerRef }) vcRef: ViewContainerRef;
#ViewChild('ngForm') ngForm: FormGroupDirective;
form: FormGroup;
constructor(protected _fb: FormBuilder) {
this.form = this._fb.group({
name: [],
price: [],
date: [],
});
}
ngAfterContentInit() {
setTimeout(() => this.vc.createEmbeddedView(this.templateChild));
}
}
base-fee-form.component.html
<form [formGroup]="form" #ngForm="ngForm">
<div class="control-group">
<span>Name: </span>
<input type="text" formControlName="name" />
</div>
<div class="control-group">
<span>Price: </span>
<input type="text" formControlName="price" />
</div>
<div class="control-group">
<span>Date: </span>
<input type="date" formControlName="date" />
</div>
<div #mountRef></div>
</form>
book-fee-form.component.ts
#Component({
selector: 'app-book-fee-form',
templateUrl: './book-fee-form.component.html',
styleUrls: ['./book-fee-form.component.css'],
encapsulation: ViewEncapsulation.None,
})
export class BookFeeFormComponent {
constructor(
// private _formDirective: FormGroupDirective,
private _fb: FormBuilder
) {
// this._formDirective.form.addControl('pageCount', this._fb.control(0));
// this._formDirective.form.addControl('bookTitle', this._fb.control(null));
}
ngOnInit() {}
}
book-fee-form.component.html
<app-base-fee-form>
<ng-template #feeChild>
<div class="control-group">
<span>Page Count: </span>
<input type="text" formControlName="pageCount" />
</div>
<div class="control-group">
<span>Book Title: </span>
<input type="text" formControlName="bookTitle" />
</div>
</ng-template>
</app-base-fee-form>
How do I access the parent NgForm to append the needed controls to the existing FormGroup? Rather, is there an easier way to do this? I'm trying to avoid creating components for each form that share nearly identical templates and functions.
I've created a Stackblitz to show my problem: StackBlitz
One solution would be to represent the controls as objects, and generate them dynamically
export type control = {
label: string;
type: string;
formControlName: string;
};
base-fee-form.component.html
<form [formGroup]="form" #ngForm="ngForm">
<ng-container *ngFor="let control of controls">
<div class="control-group">
<span>{{ control.label }}: </span>
<input
type="{{ control.type }}"
formControlName="{{ control.formControlName }}"
/>
</div>
</ng-container>
</form>
Define your common controls in the base component
export class BaseFeeFormComponent {
controls: control[] = [
{ label: 'Name', type: 'text', formControlName: 'name' },
{ label: 'Price', type: 'text', formControlName: 'price' },
{ label: 'Date', type: 'date', formControlName: 'date' },
];
form: FormGroup;
constructor(protected _fb: FormBuilder) {
this.form = this._fb.group({
name: [],
price: [],
date: [null],
});
}
}
Then extend the base component to add new controls
export class BookFeeFormComponent extends BaseFeeFormComponent {
constructor(protected _fb: FormBuilder) {
super(_fb);
this.controls = this.controls.concat([
{ label: 'Page Count', type: 'text', formControlName: 'pageCount' },
{ label: 'Book Title', type: 'text', formControlName: 'bookTitle' },
]);
this.form = this._fb.group({
...this.form.controls,
pageCount: [],
bookTitle: [],
});
}
}
You could make custom html for each component, or just point the child components to the base html
#Component({
selector: 'app-book-fee-form',
templateUrl: '../base-fee-form/base-fee-form.component.html',
...
Stackblitz: https://stackblitz.com/edit/angular-ivy-rwm5jw?file=src/app/book-fee-form/book-fee-form.component.ts

How to build reusable field for reactive form in Angular

I am trying to build a reusable field component for reactive form but I am unable to get a value from custom-input component.
<form [formGroup]="form" (ngSubmit)="submit()">
<custom-input id="name" name="name" formControlName="name"></custom-input>
<button type="submit" name="button">Submit</button>
</form>
My custom input reusable componeent
import { Component, OnInit, Input } from '#angular/core';
import { FormControl } from "#angular/forms";
#Component({
selector: 'custom-input',
template: '<input type="text" [class]="class" [id]="id" [name]="name" [formControlName]="formControlName">',
styles: []
})
export class CustomInputComponent implements OnInit {
#Input() public id: string;
#Input() public class: string;
#Input() public name: string;
#Input() public formControlName: string;
constructor() { }
ngOnInit() {
}
}
You can implement ControlValueAccessor, but might not want to re-implement the methods for the native input. In order to do that, you can use the FormControlDirective to get access to valueAccessor.
formControl and formControlName are added as input properties, so it will work in both cases. If formControlName is provided, the instance of FormControl will be retrieved from the ControlContainer.
#Component({
selector: 'app-custom-input',
template: `<input type="text" [formControl]="control">`,
styleUrls: ['./custom-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: CustomInputComponent,
multi: true
}
]
})
export class CustomInputComponent implements ControlValueAccessor {
#Input() formControl: FormControl;
#Input() formControlName: string;
#ViewChild(FormControlDirective, {static: true})
formControlDirective: FormControlDirective;
private value: string;
private disabled: boolean;
constructor(private controlContainer: ControlContainer) {
}
get control() {
return this.formControl || this.controlContainer.control.get(this.formControlName);
}
registerOnTouched(fn: any): void {
this.formControlDirective.valueAccessor.registerOnTouched(fn);
}
registerOnChange(fn: any): void {
this.formControlDirective.valueAccessor.registerOnChange(fn);
}
writeValue(obj: any): void {
this.formControlDirective.valueAccessor.writeValue(obj);
}
}
Source: https://medium.com/angular-in-depth/dont-reinvent-the-wheel-when-implementing-controlvalueaccessor-a0ed4ad0fafd

Angular8 form input fields disabled

I am using Angular8 and have a form used to update a product. My problem however is this forms input fields and submit button is disabled. Can anyone advise what I am doing wrong? I would expect to be able to edit the input fields text and submit the form.
html:
<div class="bodyClass">
<h1>{{title | titlecase}}</h1>
<div class="card">
<div class="card-body">
<form *ngIf="angForm && productData" [formGroup]="angForm" novalidate>
<div class="form-group">
<label class="col-md-4">Product Name</label>
<input type="text" class="form-control" formControlName="name" #name/>
</div>
<div *ngIf="angForm.controls['name'].invalid && (angForm.controls['name'].dirty || angForm.controls['name'].touched)"
class="alert alert-danger">
<div *ngIf="angForm.controls['name'].errors.required">
Product Name is required.
</div>
</div>
<div class="form-group">
<button (click)="updateProduct(name.value)" type="submit" class="btn btn-primary"
[disabled]="angForm.invalid">
Update Product
</button>
</div>
</form>
</div>
</div>
</div>
component:
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormBuilder, Validators } from '#angular/forms';
import { Observable } from 'rxjs';
import { PermissionsService } from '../../services/permissions.service';
import { Permissions } from '../../model/permissions';
import { ProductsService } from '../../services/products.service';
import { DataService } from '../../services/data.service';
import { Product } from '../../model/product';
#Component({
selector: 'app-productsupdate',
templateUrl: './productsupdate.component.html',
styleUrls: ['./productsupdate.component.css']
})
export class ProductsupdateComponent implements OnInit {
angForm: FormGroup;
private permissionsObservable: Observable<Permissions>;
private showData: boolean = true;
private title: string;
private productData: Product;
constructor(private _permissionsService: PermissionsService, private _productService: ProductsService,
private dataService: DataService, private fb: FormBuilder) {
this.createForm();
}
ngOnInit() {
this.title = 'Update Product';
if (this.dataService.serviceData) {
console.log(this.dataService.serviceData);
this.productData = this.dataService.serviceData;
}
}
ngDoCheck() {
if (this.productData) {
this.angForm.get('name').setValue(this.productData.name);
}
}
createForm() {
this.angForm = this.fb.group({
name: ['', Validators.required ],
decription: ['', Validators.required ],
status: ['', Validators.required ]
});
}
updateProduct() {
console.log('submit');
}
}
Screenprint:
You can see that the button is disabled and the input text is non-editable.
You are using ngDoCheck, so everytime angular runs it, for example when you are trying to type, and productData is populated,
this.angForm.get('name').setValue(this.productData.name);
is run, and thus always setting the value making it seem that you cannot edit the field.
Since this can also be a race condition, as forms are asynchronous, I suggest building the form after getting value to productData (if getting value). So remove the createForm() function from constructor and add it later on:
if (this.dataService.serviceData) {
console.log(this.dataService.serviceData);
this.productData = this.dataService.serviceData;
}
this.createForm();
Modify the createForm function a bit:
createForm() {
this.angForm = this.fb.group({
name: [this.productData.name || '', Validators.required ],
description: [this.productData.description || ''],
status: [this.productData.status || '']
});
}

validation not propagated with custom input component - Angular 4

I have a custom text-area component, with text-area input inside. I have created a custom validator to check the max length (not the html one) of the text.
All work fine, the problem is that the inner input is set to invalid (with ng-invalid) while che component itself don't and so also the form that contains the component remains valid.
It's seems to work with the built-it required validator, placed on both the component and the input.
How can I make the changes in a custom input to be reflected on the external form?
Thanks!
//sorry for my english!
Edit: I made a plunker: https://plnkr.co/edit/NHc25bo8K9OsgcxSWyds?p=preview
This is the custom text-area component html:
<textarea
[disabled]='disabled'
[required]='required'
[placeholder]="placeholder"
(changes)="onInput($event)"
(keyup)="onInput($event)"
[(ngModel)] = "data"
[name]="name"
#input="ngModel"
customMaxLength="{{maxLength}}"
></textarea>
<span *ngIf="input.errors && (input.dirty || input.touched)">
<span *ngIf="input.errors?.required" class="error-message">Required!</span>
<span *ngIf="input.errors?.customMaxLength" class="error-message">Max Length Reached({{maxLength}})</span>
</span>
This is the code of the component
import { Component, Input, forwardRef, ViewChild } from '#angular/core';
import { NgModel, ControlValueAccessor, NG_VALUE_ACCESSOR, AbstractControl } from '#angular/forms';
#Component({
selector: 'custom-text-area',
templateUrl: './custom-text-area.component.html',
styleUrls: ['./custom-text-area.component.less'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextAreaComponent),
multi: true
}
]
})
export class TextAreaComponent implements ControlValueAccessor{
#Input() required = false;
#Input() name;
#Input() data;
#Input() disabled;
#Input() placeholder = '';
#Input() errorMsg;
#Input() maxLength = null;
#ViewChild('input') input: NgModel;
constructor() {
this.data = '';
}
propagateChange = (_: any) => {};
writeValue(value: any): void {
if (value !== undefined) {
this.data = value;
} else {
this.data = '';
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onInput(e) {
this.data = e.target.value || '';
this.propagateChange(this.data);
}
}
This is the validator
import { NG_VALIDATORS, Validator, FormControl } from '#angular/forms';
import { Directive, forwardRef, Input } from '#angular/core';
#Directive({
selector: '[customMaxLength][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => MaxLengthValidatorDirective), multi: true}
]
})
export class MaxLengthValidatorDirective implements Validator{
#Input() customMaxLength: number;
ngOnInit(){
}
validate(c: FormControl): { [key: string]: any; } {
if(c.value && this.customMaxLength){
return c.value.length < this.customMaxLength ? null : { customMaxLength:{ valid: false } };
} else {
return null;
}
}
}
Aaand finally this is an use:
<form #form="ngForm">
<custom-text-area [maxLength]="3" required ngModel name="textArea"></custom-text-area>
</form>
The main problem is how you are using the NgModel. You are using it in both the custom component and inside your form. You should only be using it inside of your form. Meaning, textarea should not have an NgModel.
No:
<textarea
[disabled]='disabled'
[required]='required'
[placeholder]="placeholder"
(changes)="onInput($event)"
(keyup)="onInput($event)"
[(ngModel)] = "data"
[name]="name"
#input="ngModel"
customMaxLength="{{maxLength}}"
></textarea>
Yes:
<textarea
[disabled]='disabled'
[required]='required'
[placeholder]="placeholder"
(changes)="onInput($event)"
(keyup)="onInput($event)"
[name]="name"
customMaxLength="{{maxLength}}"
></textarea>
Here is a working example:
https://plnkr.co/edit/lWZpEpPdnfG7YDiH21jB?p=preview

Categories

Resources