I am developing an application in angular as a beginner. The application takes notes and stores them in DB.
I am facing an issue during addition. If there are no existing data in DB and I have added two new notes, both the notes are displayed as same in UI. Although the new note and old note is present in DB.
I observed one thing. The data returned from DB is in JSON format.
On adding new data, the note array is displayed as -
0: {}
1: {}
2: Note{}
3: Note{}
NoteService -
export class NotesService {
private baseURL = 'http://localhost:8082/api/v1/note';
notes: Note[];
notesSubject: BehaviorSubject<Note[]>;
constructor(private http: HttpClient) {
this.notes = [];
this.notesSubject = new BehaviorSubject<Note[]>([]);
}
private extractData(res: Response) {
let body = res;
return body;
}
fetchNotesFromServer(): Observable<Note[]> {
return this.http.get(`${this.baseURL}/${sessionStorage.getItem("userId")}`)
.map(this.extractData)
.catch(this.handleErrorObservable);
;
}
getNotes(): BehaviorSubject<Note[]> {
this.fetchNotesFromServer().subscribe(
notes => { this.notes = notes; this.notesSubject.next(this.notes) },
error => this.handleErrorObservable(error));
return this.notesSubject;
}
addNote(note: Note): Observable<Object> {
return this.http.post(this.baseURL, note, { observe: 'response' });
}
private handleErrorObservable(error: HttpErrorResponse | any) {
console.error(error.message || error);
return Observable.throw(new HttpErrorResponse(error));
}
}
NoteTakerComponent : addNote() -
addNote(): void {
this.errMessage = '';
if (this.validateNote()) {
this.note.noteCreatedBy = sessionStorage.getItem("userId");
this.note.noteTitle = this.noteTitle;
this.note.noteContent = this.noteContent;
this.note.noteStatus = this.state;
this.note.category = this.editCategory;
this.note.reminder = this.editReminder;
let maxId = 0;
if (this.noteService.notes.length > 0) {
this.noteService.notes.forEach(note => {
if (note.noteId > maxId) {
maxId = note.noteId;
}
});
}
this.note.noteId = ++maxId;
this.noteService.addNote(this.note).subscribe(response => {
this.noteService.notes.push(this.note);
this.noteService.notesSubject.next(this.noteService.notes);
console.log('note taker', this.noteService.notes);
this.reset();
},
error => this.handleErrorResponse(error));
}
}
NoteTaker view -
<!-- Expansion panel to add new notes -->
<div class="keep-note-expansion-panel">
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Take a note</mat-panel-title>
</mat-expansion-panel-header>
<div class="keep-note-form-container">
<mat-form-field>
<input matInput class="note-full-width form-control" name="title" placeholder="title" [(ngModel)]="noteTitle"
required>
</mat-form-field>
<mat-form-field>
<textarea matInput class="note-full-width form-control" name="text" placeholder="text"
[(ngModel)]="noteContent" required></textarea>
</mat-form-field>
<mat-form-field>
<mat-select name="Status" placeholder="Select state" [(ngModel)]="state">
<mat-option *ngFor="let state of states" [value]="state">
{{ state }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select name="category" placeholder="Select Category" [(ngModel)]="editCategory">
<mat-option *ngFor="let category of categories" [value]="category">
{{ category.categoryName }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select name="reminder" placeholder="Select Reminder" [(ngModel)]="editReminder" multiple>
<mat-option *ngFor="let reminder of reminders" [value]="reminder">
{{ reminder.reminderName }}
</mat-option>
</mat-select>
</mat-form-field>
<button mat-button (click)="addNote()">Done</button>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
NoteView Component -
export class NoteViewComponent implements OnInit {
notes: Note[];
errorMessage: String;
constructor(private noteService: NotesService) {
}
ngOnInit(): void {
this.noteService.getNotes().subscribe(
notes => {
this.notes = notes;
console.log('note view', this.notes);
},
error => this.handleErrorResponse(error));
}
}
Expected result is to show newly added notes with the old notes.
There was a basic error where the same note object was modified instead of creating a new object.
Modified NoteTaker component -
addNote(): void {
this.errMessage = '';
this.note = new Note();
if (this.validateNote()) {
this.note.noteCreatedBy = sessionStorage.getItem("userId");
this.note.noteTitle = this.noteTitle;
this.note.noteContent = this.noteContent;
this.note.noteStatus = this.state;
this.note.category = this.editCategory;
this.note.reminder = this.editReminder;
let maxId = 0;
if (this.noteService.notes.length > 0) {
this.noteService.notes.forEach(note => {
if (note.noteId > maxId) {
maxId = note.noteId;
}
});
}
this.note.noteId = ++maxId;
this.noteService.addNote(this.note).subscribe(response => {
this.noteService.notes.push(this.note);
this.noteService.notesSubject.next(this.noteService.notes);
console.log('note taker', this.noteService.notes);
this.reset();
},
error => this.handleErrorResponse(error));
}
}
Related
Thanks for reading.
I have a page where a user can input and select an address from an autocomplete.
The source of the autocomplete is from an external API, which is called using the valueChanges event.
The resultant behaviour is that of a predictive address lookup based on user input.
This works currently for this singular purpose.
<mat-form-field>
<input matInput placeholder="Search Multi" aria-label="State" [matAutocomplete]="auto" [formControl]="searchMoviesCtrl" type="text">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
<mat-option *ngIf="isLoading" class="is-loading">Loading...</mat-option>
<ng-container *ngIf="!isLoading">
<mat-option *ngFor="let suggestion of filteredMovies" [value]="suggestion.text">
<span><b>{{suggestion.text}}</b></span>
</mat-option>
</ng-container>
</mat-autocomplete>
</mat-form-field>
<br>
<ng-container *ngIf="errorMsg; else elseTemplate">
{{errorMsg}}
</ng-container>
<ng-template #elseTemplate>
<h5>Selected Value: {{searchMoviesCtrl.value}}</h5>
</ng-template>
import { Component, OnInit } from '#angular/core';
import { FormControl } from '#angular/forms';
import { HttpClient } from '#angular/common/http';
import { Router } from '#angular/router';
import { debounceTime, tap, switchMap, finalize } from 'rxjs/operators';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
searchMoviesCtrl = new FormControl();
filteredMovies: any;
isLoading = false;
errorMsg: string;
constructor(private http: HttpClient)
{ }
ngOnInit() {
this.searchMoviesCtrl.valueChanges
.pipe(
debounceTime(500),
tap(() => {
this.errorMsg = "";
this.filteredMovies = [];
this.isLoading = true;
}),
switchMap(value => this.http.get("http://searchaddressapiurl?text=" + this.searchMoviesCtrl.value)
.pipe(
finalize(() => {
this.isLoading = false
}),
)
)
)
.subscribe(data => {
console.log('Search text :' + this.searchMoviesCtrl.value);
console.log(data);
if (data['suggestions'] == undefined) {
this.errorMsg = data['Error'];
this.filteredMovies = [];
console.log('coming here ERROR');
} else {
this.errorMsg = "";
this.filteredMovies = data['suggestions'];
console.log('coming here');
}
console.log(this.filteredMovies);
});
}
displayFn(suggestion : string) : string {
console.log('Display - ' + suggestion);
return suggestion ? suggestion : '';
}
}
However, I want to allow the user to add additional autocomplete inputs, which will use/call the same API on value change.
How best can I go about this?
I have been able to get the adding of multiple inputs going by doing the below.
I'm just not sure how to go about hooking up these inputs to a valueChange function which calls the search API passing through the entered text...ideally one function that caters for all inputs
<form [formGroup]="autocompleteForm" novalidate >
<div formArrayName="sites">
<div *ngFor="let unit of autocompleteForm.controls.sites.controls; let i=index" class="form-group">
<div [formGroupName]="i">
<div style="width: 100%;">
<mat-form-field>
<input matInput placeholder="Search Multi" aria-label="State" [matAutocomplete]="auto" formControlName="site" type="text" (input)="searchSite(this, i)">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
<mat-option *ngIf="isLoading" class="is-loading">Loading...</mat-option>
<ng-container *ngIf="!isLoading">
<mat-option *ngFor="let suggestion of filteredAddresses" [value]="suggestion.text">
<span><b>{{suggestion.text}}</b></span>
</mat-option>
</ng-container>
</mat-autocomplete>
</mat-form-field>
</div>
</div>
</div>
<button (click)="addSite()">Add Site</button>
</div>
<ng-container *ngIf="errorMsg; else elseTemplate">
{{errorMsg}}
</ng-container>
<ng-template #elseTemplate>
<h5>Selected Value: {{site.value}}</h5>
</ng-template>
</form>
ngOnInit(){
this.autocompleteForm = this.formBuilder.group({
sites: this.formBuilder.array([
// load first row at start
this.getSite()
])
});
displayFn(suggestion : string) : string {
console.log('Display - ' + suggestion);
return suggestion ? suggestion : '';
}
public searchSite(obj, index)
{
console.log(obj);
console.log(index + ' - ' + obj);
}
private getSite() {
return this.formBuilder.group({
site: ['', [Validators.required]
});
}
addSite() {
const control = <FormArray>this.autocompleteForm.controls['sites'];
control.push(this.getSite());
}
UPDATE
I've udpdated the searchSite method that gets called on input changes...
It allows for the inputs to call the searchSite method.
It does the job of getting back the necessary resultset but it makes many unnecessary API calls seemingly, I figure this because of the (input) onchange call and the .valueChanges event hookup.
Still a work in progress at this point but just putting up some of the progress.
Ideas still welcome.
public searchSite(obj : MultiAutoCompleteComponent, index)
{
console.log(obj);
console.log(index + ' - ' + obj);
console.log(obj.autocompleteForm.controls.sites.value[index].site);
console.log('Input value : ' + obj.autocompleteForm.controls);
var searchText = obj.autocompleteForm.controls.sites.value[index].site;
if(searchText.length < 2 || searchText == '' || searchText == null || searchText == undefined || searchText === -1)
{
console.log('Minimum not provided');
return;
}
obj.autocompleteForm.controls.sites.valueChanges
.pipe(
debounceTime(500),
tap(() => {
this.errorMsg = "";
this.filteredAddresses = [];
this.isLoading = true;
}),
switchMap(value => this.http.get("http://searchaddressapi?text=" + searchText)
.pipe(
finalize(() => {
this.isLoading = false
}),
)
)
)
.subscribe(data => {
console.log('Search text :' + this.site.value);
console.log(data);
if (data['suggestions'] == undefined) {
this.errorMsg = data['Error'];
this.filteredAddresses = [];
console.log('Search site coming here ERROR');
} else {
this.errorMsg = "";
this.filteredAddresses = data['suggestions'];
console.log('Search site coming here');
}
console.log(this.filteredAddresses);
});
}
UPDATE 2
So I have made some changes, this results in the autocomplete working as expected for my use case. (Adding dynamic Multi autocompletes which call the same API)
Just one thing I would still like to address...the calls to the API on each char input.
I tried to address this by using debounceTime but this didn't seem to have an effect, it delayed the calls but they were still made (all of them per char) instead of ignoring those calls sent during the debounceTime. At least that is what I thought would/should happen?
Is my understanding of the deboundTime behaviour incorrect?
import { Observable, Subject } from 'rxjs';
searchTextChanged = new Subject<string>();
public searchSite(obj : MultiAutoCompleteComponent, index)
{
console.log(obj);
console.log(index + ' - ' + obj);
console.log(obj.autocompleteForm.controls.sites.value[index].site);
console.log('Input value : ' + obj.autocompleteForm.controls);
var searchText = obj.autocompleteForm.controls.sites.value[index].site;
const items = this.autocompleteForm.get('sites') as FormArray;
console.log('No of sites added: ' + items.length);
if(searchText.length < 5 || searchText == '' || searchText == null || searchText == undefined || searchText === -1)
{
console.log('Minimum not provided, no serarch conducted');
return;
}
else
{
this.searchTextChanged.next(searchText);
this.searchTextChanged
.pipe(
debounceTime(1000),
tap(() => {
this.errorMsg = "";
this.filteredAddresses = [];
this.isLoading = true;
}),
switchMap(value => this.http.get("http://searchaddressapi?text=" + searchText)
.pipe(
finalize(() => {
this.isLoading = false
}),
)
)
)
.subscribe(data => {
console.log('Search text :' + searchText);
console.log(data);
if (data['suggestions'] == undefined) {
this.errorMsg = data['Error'];
this.filteredAddresses = [];
console.log('Search site coming here ERROR');
} else {
this.errorMsg = "";
this.filteredAddresses = data['suggestions'];
console.log('Search site coming here');
}
console.log(this.filteredAddresses);
});
}
}
What am I doing wrong re. the debounceTime delay implementation?
UPDATE 3 - SOLVED
I got this to work as needed!
Dynamically add additional autocompletes which make use of the same API for data result set.
The debounceTime reduces the number of API calls when a user is inputting searchText.
I'm sure you can clean this up and as suggested by one commenter put the API call in a service but here it is anyway.
//multi-auto-complete.component.css
.example-form {
min-width: 150px;
max-width: 500px;
width: 100%;
}
.example-full-width {
width: 100%;
}
//multi-auto-complete.component.html
<div>
<form [formGroup]="autocompleteForm" novalidate >
<div formArrayName="sites">
<div *ngFor="let unit of autocompleteForm.controls.sites.controls; let i=index" class="form-group">
<div [formGroupName]="i">
<div style="width: 100%;">
<mat-form-field>
<input matInput placeholder="Search Multi" aria-label="State" [matAutocomplete]="auto" formControlName="site" type="text" (input)="searchSite(this, i)">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
<mat-option *ngIf="isLoading" class="is-loading">Loading...</mat-option>
<ng-container *ngIf="!isLoading">
<mat-option *ngFor="let suggestion of filteredAddresses" [value]="suggestion.text">
<span><b>{{suggestion.text}}</b></span>
</mat-option>
</ng-container>
</mat-autocomplete>
</mat-form-field>
</div>
</div>
</div>
<button (click)="addSite()">Add Site</button>
</div>
<ng-container *ngIf="errorMsg; else elseTemplate">
{{errorMsg}}
</ng-container>
<ng-template #elseTemplate>
<h5>Selected Value: {{site.value}}</h5>
</ng-template>
</form>
</div>
//multi-auto-complete.component.ts
import { Component, OnInit, ɵConsole } from '#angular/core';
import { FormControl } from '#angular/forms';
import { HttpClient } from '#angular/common/http';
import { Router } from '#angular/router';
import { debounceTime, tap, switchMap, finalize } from 'rxjs/operators';
import { FormGroup, FormBuilder, FormArray, Validators } from '#angular/forms';
import { Observable, Subject } from 'rxjs';
export interface Suggestion {
text: string;
magicKey: string;
isCollection: boolean;
}
#Component({
selector: 'app-multi-auto-complete',
templateUrl: './multi-auto-complete.component.html',
styleUrls: ['./multi-auto-complete.component.css']
})
export class MultiAutoCompleteComponent implements OnInit {
site = new FormControl();
filteredAddresses: any;
isLoading = false;
errorMsg: string;
autocompleteForm: FormGroup;
results$: Observable<any>;
searchTextChanged = new Subject<string>();
constructor(private http: HttpClient, private router: Router, private formBuilder: FormBuilder)
{ }
ngOnInit(){
this.autocompleteForm = this.formBuilder.group({
sites: this.formBuilder.array([
// load first row at start
this.getSite()
])
});
this.results$ = this.searchTextChanged.pipe(
debounceTime(500),
switchMap(searchText => this.http.get("http://searchaddressapi?text=" + searchText)))
console.log(this.results$.subscribe(data => {
this.filteredAddresses = data['suggestions'];
console.log(data)
}));
}
displayFn(suggestion : string) : string {
console.log('Displaying selection - ' + suggestion);
this.filteredAddresses = [];
return suggestion ? suggestion : '';
}
public searchSite(obj : MultiAutoCompleteComponent, index)
{
console.log(obj);
console.log(index + ' - ' + obj);
console.log(obj.autocompleteForm.controls.sites.value[index].site);
console.log('Input value : ' + obj.autocompleteForm.controls);
var searchText = obj.autocompleteForm.controls.sites.value[index].site;
//const items = this.autocompleteForm.get('sites') as FormArray;
//console.log('No of sites added: ' + items.length);
if(searchText.length < 5 || searchText == '' || searchText == null || searchText == undefined || searchText === -1)
{
console.log('Minimum characters not provided, no search conducted');
return;
}
else
{
this.searchTextChanged.next(searchText);
}
}
/**
* Create form site
*/
private getSite() {
return this.formBuilder.group({
site: ['', [Validators.required]]
});
}
/**
* Add new site row into form
*/
addSite() {
const control = <FormArray>this.autocompleteForm.controls['sites'];
control.push(this.getSite());
this.filteredAddresses = [];
}
}
I got this to work as needed!
Dynamically add additional autocompletes which make use of the same API for data result set.
The debounceTime reduces the number of API calls when a user is inputting searchText.
I'm sure you can clean this up and as suggested by one commenter put the API call in a service but here it is anyway.
Please see UPDATE 3 in my Question Post for the code!
I have from group "additionalForm" in that i have formArray called "validations"
here the formArray is dynamic which is binding the values to validtionsField array. In the validtionsField array i have three objects in which i have two values which needs to be compared and they are Min-length and Max-Length.
ex. If i enter min length greater then the max length it should give error.
here is code for the above functionality
import {
Component,
OnInit,
Inject
} from "#angular/core";
import {
FormControl,
FormArray,
FormGroup,
FormBuilder,
Validators,
AbstractControl,
ValidationErrors,
NgControlStatus,
ValidatorFn
} from "#angular/forms";
import {
MatDialogRef,
MAT_DIALOG_DATA,
MatSnackBar
} from "#angular/material";
#Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
validtionsField = [{
validField: "Min Length",
type: false,
fieldType: "input",
value: 1,
keyval: "minLength"
},
{
validField: "Max Length",
type: false,
fieldType: "input",
value: 50,
keyval: "maxLength"
},
{
validField: "DataType",
type: false,
fieldType: "dropDown",
dataTypeList: [],
dataTypeId: "minLength",
keyval: "dataTypeId",
value: 874
}
];
dataType = [{
id: 3701,
localeId: 1,
tenantId: 1,
parentCategoryId: null,
parentContentId: 873,
name: "Alphabets",
description: null
},
{
id: 3702,
localeId: 1,
tenantId: 1,
parentCategoryId: null,
parentContentId: 874,
name: "Alphanumeric",
description: null
}
];
additionalForm: FormGroup = this.fb.group({
fieldName: ["", [Validators.required]],
validations: this.fb.array([])
});
constructor(public fb: FormBuilder) {}
ngOnInit() {
let frmArray = this.additionalForm.get("validations") as FormArray;
for (let data of this.validtionsField) {
frmArray.push(this.initSection(data));
}
}
initSection(data) {
return this.fb.group({
validField: [data.validField, [Validators.required]],
type: [data.type, [Validators.required]],
value: [data.value, [Validators.required]],
dataTypeList: [this.dataType, [Validators.required]],
fieldType: [data.fieldType, [Validators.required]],
validArray: []
}, {
validator: this.customValidator
});
}
checkFieldType(data): any {
return data === "dropDown";
}
// trying to access using below functioa to compare values min and max length
public customValidator(control: AbstractControl): ValidationErrors | null {
const newValue = control.get("value") ? control.get("value").value : null;
const values = control.get("value") ? control.get("value").value : [];
console.log("1 " + newValue);
console.log(values);
for (let i = 0, j = values.length; i < j; i++) {
if (newValue === values[i]) {
return {
duplicate2: true
};
}
}
return null;
}
}
<form [formGroup]="additionalForm">
<mat-form-field>
<input formControlName='fieldName' placeholder="Field Name" required matInput>
</mat-form-field>
<div class="row">
<div class="col-md-12 col-sm-12">
\
<div formArrayName="validations">
<ng-container *ngFor="let validationForm of additionalForm.controls.validations.controls; let i = index">
<div class="valid-data" [formGroupName]="i">
<span>
<label>{{validationForm.value.validField }}</label>
</span>
<span>
<ng-container *ngIf="checkFieldType(validationForm.value.fieldType ); else input">
<mat-form-field class="select-dataType">
<mat-select required formControlName='value' placeholder="Datatype">
<mat-option *ngFor="let fieldTypeData of validationForm.value.dataTypeList"
[value]='fieldTypeData.parentContentId'>
{{fieldTypeData.name}}</mat-option>
</mat-select>
</mat-form-field>
</ng-container>
<ng-template #input>
<mat-form-field>
<input required formControlName='value' pattern= "[0-9]+" matInput>
</mat-form-field>
</ng-template>
</span>
<div *ngIf="validationForm.get('value')?.touched ">
<div class="error" *ngIf="validationForm.get('value').hasError('required')">
{{validationForm.value.validField}} is required
</div>
</div>
</div>
</ng-container>
</div>
</div>
</div>
</form>
above is the TS and HTML code and below is the function which i am trying to get old and new value from control but its failing, its giving me value from same input field from same min-length
/// trying this below functio to compare the min and max length.
public customValidator(control: AbstractControl): ValidationErrors | null {
const newValue = control.get('value') ? control.get('value').value : null;
const values = control.get('value') ? control.get('value').value : [];
console.log("1 " + newValue);
console.log(values);
for (let i = 0, j = values.length; i < j; i++) {
if (newValue === values[i]) {
return {
'duplicate2': true
};
}
}
return null;
}
Please help me to compare values from dynamic form array, and here what ever values enters to from-array object are all bind to formcontrolName "value"
here is the link for code :
https://stackblitz.com/edit/angular6-material-components-demo-wgtafn
Since you have two fields minLength & maxLength which validation depends on each other, you can add a validator to the parent group and use a custom ErrorStateMatcher to translate the parent group errors to the children. I also used a FormGroup instead of FormArray, in this case it's more convenient.
#Component({...})
export class AppComponent {
...
readonly invalidLengthMatcher: ErrorStateMatcher = {
isErrorState: () => {
const control = this.additionalForm.get('validations');
return control.hasError('invalidLength');
}
};
readonly controlFields = this.validtionsField.map(field => ({
field,
control: new FormControl(field.value, Validators.required),
errorMatcher: this.errorMatcherByFieldId(field.keyval)
}));
private readonly controlMap = this.controlFields.reduce((controls, controlField) => {
controls[controlField.field.keyval] = controlField.control;
return controls;
}, {});
readonly additionalForm = new FormGroup({
fieldName: new FormControl("", [Validators.required]),
validations: new FormGroup(this.controlMap, {
validators: (group: FormGroup) => {
const [minLength, maxLength] = ['minLength', 'maxLength'].map(fieldId => {
const control = group.get(fieldId);
return Number(control.value);
});
if (minLength > maxLength) {
return {
'invalidLength': true
};
} else {
return null;
}
}
})
});
private errorMatcherByFieldId(fieldId: string): ErrorStateMatcher | undefined {
switch (fieldId) {
case 'minLength':
case 'maxLength':
return this.invalidLengthMatcher;
}
}
}
<form [formGroup]="additionalForm">
<mat-form-field>
<input formControlName='fieldName' placeholder="Field Name" required matInput>
</mat-form-field>
<div class="row">
<div class="col-md-12 col-sm-12">
<div formGroupName="validations" >
<div *ngFor="let controlField of controlFields" class="valid-data">
<span>
<label>{{controlField.field.validField}}</label>
</span>
<span [ngSwitch]="controlField.field.fieldType">
<mat-form-field *ngSwitchCase="'dropDown'" class="select-dataType">
<mat-select required placeholder="Datatype" [formControlName]="controlField.field.keyval">
<mat-option *ngFor="let fieldTypeData of dataType"
[value]='fieldTypeData.parentContentId'>{{fieldTypeData.name}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngSwitchCase="'input'">
<input matInput
required
type="number"
pattern= "[0-9]+"
[formControlName]="controlField.field.keyval"
[errorStateMatcher]="controlField.errorMatcher">
</mat-form-field>
</span>
...
</div>
</div>
</div>
</div>
</form>
StackBlitz
You had to attach this validator to form array so that you can have access to all controls.
My Custom Validator :-
export function customValidateArray(): ValidatorFn {
return (formArray:FormArray):{[key: string]: any} | null=>{
console.log('calling');
let valid:boolean=true;
setTimeout(()=>console.log(formArray),200);
let minIndex = Object.keys(formArray.controls).findIndex((key) => formArray.controls[key].value.validField.toLowerCase()==="min length");
let minLengthValue = formArray.controls[minIndex].value.value;
let maxLengthValue = formArray.controls[Object.keys(formArray.controls).find((key) => formArray.controls[key].value.validField.toLowerCase()==="max length")].value.value;
return minLengthValue < maxLengthValue ? null : {error: 'Min Length Should be less than max length', controlName : formArray.controls[minIndex].value.validField};
}
};
I added it to form array ngOnInit of your code :-
ngOnInit() {
let frmArray = this.additionalForm.get("validations") as FormArray;
for (let data of this.validtionsField) {
frmArray.push(this.initSection(data));
}
frmArray.setValidators(customValidateArray());
}
Use it in template like :-
<div class="error" *ngIf="additionalForm.controls.validations.errors && validationForm.value.validField === additionalForm.controls.validations.errors.controlName">
{{additionalForm.controls.validations.errors.error}}
</div>
Working stackblitz :-
https://stackblitz.com/edit/angular6-material-components-demo-p1tpql
Getting input from a reactive form inside a ngFor ,I have to display "correct" or "incorrect" based on the comparison of the user answer with the exercise.question.answer value.
My idea is to create a boolean reactively but I'm struggling with the execution. I'm not being able to compare index[x] of array a with index [x] of array b every time these arrays are created.
This is the template:
<form
fxLayout="column"
fxLayoutGap="2px"
[formGroup]="exerciseForm"
(ngSubmit)="onSubmit(exerciseForm.value)"
>
<ul *ngFor="let exercise of exercises">
<li>{{ exercise.instruction }}</li>
<ul *ngFor="let question of exercise.questions; let i = index">
<li>
{{ question.prefix }}
<mat-form-field>
<input
name="answer"
type="text"
id="answer"
matInput
[formControlName]="question.id"
/>
</mat-form-field>
{{ question.sufix }} -
{{ question.translation }}
<div *ngIf="isAnswered">
<div *ngIf="isCorrect"><p>Correct</p></div>
<div *ngIf="!isCorrect"><p>Incorrect</p></div>
</div>
</li>
</ul>
</ul>
<button type="submit" mat-raised-button color="primary">Submit</button>
</form>
this is the ts (it contains some of the methods I've been attempting)
export class ExerciseTestsComponent implements OnInit {
exerciseForm: FormGroup;
isAnswered: boolean;
isCorrect: boolean;
exercises: Exercise[] = [
new Exercise("Answer this question", [
new Question(1, "Eu", "maluco", "I am crazy", "sou"),
new Question(2, "Eu", "doidinho", "I am cuckoo", "estou")
])
];
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.createGroup();
}
getAnswersArray() {}
createGroup() {
this.exerciseForm = this.fb.group({});
this.exercises[0].questions.forEach(control =>
this.exerciseForm.addControl(control.id.toString(), this.fb.control(""))
);
}
onSubmit(answer: TestAnswer) {
this.isAnswered=true;
//** 1
let answers = [];
let answersInput = [];
this.exercises[0].questions.forEach(pergunta => {
//** 2
answers.push(pergunta.answer);
console.log(answers);
});
//** 3
var bosta = Object;
bosta = this.exerciseForm.value;
console.log(bosta[1]);
if ((answers[1] = bosta[1])) {
console.log("pqp");
}
let incoming = this.exerciseForm.value;
for (var o in incoming) {
answersInput.push(incoming[o]);
console.log(answersInput);
}
answersInput.forEach(a1 =>
answers.forEach(a2 => {
if (a1 === a2) {
console.log("yay");
} else {
console.log("boo");
}
})
);
}
}
//** for every object created, I have to check if answer = */
stackblitz:
https://stackblitz.com/edit/angular-dzzzql
Then when you submit, you can compare both answers
onSubmit(answer: Answer) {
let answers = [];
console.log(this.exercises)
let answersInput = []
this.exercises[0].questions.forEach((pergunta, index) => {
answers.push(pergunta.answer)
console.log(answers)
return answers
})
let i = 0;
for (const field in this.exerciseForm.controls) { // 'field' is a string
console.log(this.exerciseForm.controls[field].value == answers[i]);
i++;
}
}
Working demo in Stackblitz
When I select an option instead of showing me the name it shows me the id. That's because I bind [value] = "option.id". I'm sending an id to the server and want to show name.
<mat-form-field class="example-full-width">
<input
matInput
placeholder="Pretrazite proizvod koji zelite naruciti samo kucajte pocetna slova"
formControlName="id"
[matAutocomplete] = "auto"
>
<mat-autocomplete #auto="matAutocomplete" >
<mat-option *ngFor="let option of allProducts; let i = index" [value]="option.id" (onSelectionChange)="getPosts(i)">
{{ option.name }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
getProducts() {
this.product.getAllProducts().subscribe((data: any) => {
this.allProducts = data.products;
console.log(data.products);
});
}
I also have (onSelectionChange) = "getPosts (i)" function
getPosts(index){
this.selectedProduct = index;
}
My question is how do I forward the id and display the name in mat-autocomplete-mat-option options.
I suppose this onchange is my function, the problem is that I have dynamic fields
Looking my other code
ngOnInit() {
this.getProducts();
this.form = this.fb.group({
address: [null],
phone: [null],
city: [null],
data: this.fb.array([this.createContact()])
});
this.contactList = this.form.get('data') as FormArray;
}
createContact(): FormGroup {
return this.fb.group({
id: [this.selectedProduct],
quantity: [null]
});
In case you need the whole code, ask me, but I didn't want to overwhelm you with a lot of code.
You can do something like this, it should help you:
<mat-autocomplete #auto="matAutocomplete" >
<mat-option *ngFor="let option of allProducts; let i = index" [value]="selectedProduct.name" (onSelectionChange)="getPosts(option)">
{{ option.name }}
</mat-option>
</mat-autocomplete>
First of all I want to show countries with this pipe and filter them with inserted characters and I need the ISO codes to show the flags of the countries. The problem is that I want to use a library which has all countries with ISO codes and stuff. This has the key value form.
First of all I export this data to var to be able to use the data.
export var indexedArray: { [key: string]: string }
countryStuff: Country; //currently not used
countries = [] as Array<string>
filteredCountries: Observable<string[]>;
export interface Country { //currently not used
[key: string]: string
}
ngOnInit() {
this.startDate.setFullYear(this.startDate.getFullYear() - 18);
this.buildForm();
this.filteredCountries = this.personalForm.controls['country'].valueChanges
.pipe(
startWith(''),
map(value => this._filter(value))
);
i18nIsoCountries.registerLocale(require("i18n-iso-countries/langs/en.json"));
i18nIsoCountries.registerLocale(require("i18n-iso-countries/langs/de.json"));
this.currentLanguage = this.translateService.currentLang;
indexedArray = i18nIsoCountries.getNames(this.currentLanguage);
for (let key in indexedArray) {
let value = indexedArray[key];
this.countries.push(value);
}
}
In the html I can use this like this:
<mat-option *ngFor="let item of countryStuff | keyvalue:keepOriginalOrder" [value]="item.key">
Key: <b>{{item.key}}</b> and Value: <b>{{item.value}}</b>
</mat-option>
The normal way I can also use but completly without the key value way and just like the Angular examples say (without the TS logic):
<mat-option *ngFor="let option of filteredCountries | async" [value]="option">
<span class="flag-icon flag-icon-de flag-icon-squared"></span>
{{option}}
</mat-option>
That just gives me the full country name like Algeria or something.
I have found an idea here https://long2know.com/2017/04/angular-pipes-filtering-on-multiple-keys/ but I couldn't change it for my purposes. That would be even more perfect if I could filter for key and value so maybe "DE" for key and "Ger" for value of Germany. It does not seem to be possible with existing pipes.
Edit on request (filtering):
private _filter(value: string): string[] {
var filterValue;
if (value) {
filterValue = value.toLowerCase();
} else {
filterValue = "";
}
return this.countries.filter(option => option.toLowerCase().startsWith(filterValue));
}
Also updated the ngOnInit()
I got it working with the i18n library and even flag-icon-css
TypeScript (filter class):
#Pipe({
name: 'filterLanguages'
})
export class FilterLanguages implements PipeTransform {
transform(items: any, filter: any, isAnd: boolean): any {
if (filter && Array.isArray(items)) {
let filterKeys = Object.keys(filter);
if (isAnd) {
return items.filter(item =>
filterKeys.reduce((memo, keyName) =>
(memo && new RegExp(filter[keyName], 'gi').test(item[keyName])) || filter[keyName] === "", true));
} else {
return items.filter(item => {
return filterKeys.some((keyName) => {
return new RegExp(filter[keyName], 'gi').test(item[keyName]) || filter[keyName] === "";
});
});
}
} else {
return items;
}
}
}
HTML:
<mat-form-field class="field-sizing">
<input matInput required placeholder="{{ 'REGISTRATION.COUNTRY' | translate }}" name="country"
id="country" [matAutocomplete]="auto" formControlName="country" [value]="filterText" />
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
<mat-option *ngFor="let item of countries | filterLanguages:{ name: filterText, iso: filterText, flagId: filterText } : false" [value]="item.name">
<span class="flag-icon flag-icon-{{item.flagId}} flag-icon-squared"></span>
{{ item.name }} - {{ item.iso }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
TypeScript (component):
export var countryList: {
[key: string]: string
}
declare const require;
export class YourComponent implements OnInit {
countries = [] as Array<any>
currentLanguage: string;
filterText: string;
ngOnInit() {
i18nIsoCountries.registerLocale(require("i18n-iso-countries/langs/en.json"));
i18nIsoCountries.registerLocale(require("i18n-iso-countries/langs/de.json"));
this.currentLanguage = this.translateService.currentLang;
countryList = i18nIsoCountries.getNames(this.currentLanguage);
this.buildForm();
this.createCountries();
this.personalForm.controls['country']
.valueChanges
.pipe(debounceTime(100))
.pipe(distinctUntilChanged())
.subscribe(term => {
this.filterText = term;
});
}
ngAfterContentChecked() {
this.cdRef.detectChanges();
if (this.currentLanguage != this.translateService.currentLang) {
countryList = i18nIsoCountries.getNames(this.translateService.currentLang);
this.createCountries();
this.currentLanguage = this.translateService.currentLang;
this.personalForm.get('country').updateValueAndValidity();
}
}
createCountries() {
this.countries = [];
for (let key in countryList) {
var countryName: string = countryList[key];
var isoValue: string = key;
var isoLowerValue: string = key.toLowerCase();
this.countries.push({ name: countryName, iso: isoValue, flagId: isoLowerValue })
}
}
You need the i18n library and like in my example a formControl.