I'm using angular 2 reactive forms and made a validator for a date of birth field. The validator is working, but it turns out the date of birth field is split into three new field: year, month, day. They all have their own validators. My question is, how can I change my code so my original date of birth validator works on three fields.
my original validator that checks one field.
input(2000/12/12) is valid
export function dobValidator(control) {
const val = control.value;
const dobPattern = /^\d{4}\/\d{2}\/\d{2}$/ ;
const comp = val.split('/');
const y = parseInt(comp[0], 10);
const m = parseInt(comp[1], 10);
const d = parseInt(comp[2], 10);
const jsMonth = m - 1;
const date = new Date(y, jsMonth, d);
const isStringValid = dobPattern.test(control.value);
const isDateValid = (date.getFullYear() === y && date.getMonth() === jsMonth && date.getDate() === d);
return (isStringValid && isDateValid) ? null : { invalidDob: ('Date of birth not valid') };
};
new html with 3 fields
year has a validator that checks the year
day has a validator that checks if the input is between 1 and 31
month has a validator that checks if the input is between 1 and 12.
I want to combine the above input of the three field into a new string and use my original date of birth validator.
<label>Date of birth :</label>
<div>
<div class="col-xs-1">
<input required type="text" formControlName="day" class="form-control" placeholder="dd" id="day"/>
<p *ngIf="form.controls.day.dirty && form.controls.day.errors">{{ form.controls.day.errors.invalidDay }}</p>
</div>
<div class="col-xs-1">
<input required type="text" formControlName="month" class="form-control" placeholder="mm" id="month"/>
<p *ngIf="form.controls.month.dirty && form.controls.month.errors">{{ form.controls.month.errors.invalidMonth }}</p>
</div>
<div class="col-xs-2">
<input required type="text" formControlName="year" class="form-control" placeholder="yyyy" id="year"/>
<p *ngIf="form.controls.year.dirty && form.controls.year.errors">{{ form.controls.year.errors.invalidYear }}</p>
</div>
</div>
<div>
<button type="submit" [disabled]="form.invalid">Submit</button>
</di>
I have created a validator for comparing two dates (their format is NgbDateStruct - as used in ng-bootstrap package's datepickers)
import { Directive, forwardRef, Attribute } from '#angular/core';
import { Validator, AbstractControl, NG_VALIDATORS, ValidatorFn } from '#angular/forms';
import { NgbDateStruct } from "#ng-bootstrap/ng-bootstrap";
import { toDate } from "../helpers/toDate";
export function dateCompareValidator(compareToControl: string, compareToValue: NgbDateStruct, compareType: string, reverse: boolean, errorName: string = 'dateCompare'): ValidatorFn {
return (c: AbstractControl): { [key: string]: any } => {
let compare = function (self: Date, compareTo: Date): any {
console.log('comparing ', compareType.toLowerCase());
console.log(self);
console.log(compareTo);
if (compareType.toLowerCase() === 'ge') {
if (self >= compareTo) {
return true;
} else {
return false;
}
} else if (compareType.toLowerCase() === 'le') {
if (self <= compareTo) {
return true;
} else {
return false;
}
}
return false;
};
// self value
let v = c.value;
// compare vlaue
let compareValue: Date;
let e;
if (compareToValue) {
compareValue = toDate(compareToValue);
} else {
e = c.root.get(compareToControl);
if (e) {
compareValue = toDate(e.value);
}
else {
// OTHER CONTROL NOT FOUND YET
return null;
}
}
let controlToValidate: AbstractControl = reverse ? e : c;
// validate and set result
let error = null;
let result = compare(toDate(c.value), compareValue);
if (result === true) {
console.log('clearing errors', compareToControl);
if (controlToValidate.errors) {
delete controlToValidate.errors[errorName];
if (!Object.keys(controlToValidate.errors).length) {
controlToValidate.setErrors(null);
}
}
else {
console.log('errors property not found in control', controlToValidate);
}
} else {
error = {};
error[errorName] = false;
controlToValidate.setErrors(error);
console.log(controlToValidate.errors);
console.log(controlToValidate.value);
console.log('Error Control', controlToValidate);
console.log('returning errors');
}
return reverse ? null : error;
}
}
Couldn't manage to modify much lot to best describe here as an answer but I believe you would get your query answered in this validator function code.
Note:
Function toDate() used in the code is a small function I created to convert NgbDateStruct into a javascript date object so that comparing dates can get easier. Here goes its implementation:
import { NgbDateStruct } from "#ng-bootstrap/ng-bootstrap"
export function toDate(ngbDate: NgbDateStruct): Date {
return ngbDate != null ? new Date(Date.UTC(ngbDate.year, ngbDate.month, ngbDate.day)) : null;
}
Related
I'm iterating a list of jobs and there's a search implemented on this list.
Search is working but now it only filters list based on one field.
Here's my list:
<ion-card *ngFor="let job of allJobs | search : searchTerm">
<ion-grid>
<ion-row>
<ion-col>
<div>
<span> {{job.day | uppercase}}</span>
<span> {{job.month | uppercase}}</span>
</div>
</ion-col>
<ion-col>
<div>
<span>{{job.time}}</span>
<span>{{job.name}}</span>
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-card>
I made a pipe for implementing search. Here's the code for it.
transform(items: any[], terms: string): any[] {
if(!items) return [];
if(!terms) return items;
terms = terms.toLowerCase();
return items.filter( it => {
return it.name.toLowerCase().includes(terms); // only filter name
});
}
Now the list gets filtered only based on the name field. I wanna filter the list based on day, month and time as well.
Can anyone tell me how to make this happen?
Sample Data for Jobs. Jobs is an array of objects
[
{
"id":10,
"day":"Monday",
"month":"June",
"time":"10",
"name":"John",
"email":"john#gmail.com"
},
{
"id":11,
"day":"Tuesday",
"month":"May",
"time":"12",
"name":"Jane",
"email":"jane#gmail.com"
},
{
"id":12,
"day":"Friday",
"month":"June",
"time":"16",
"name":"",
"email":"john#gmail.com"
},
{
"id":13,
"day":"Tuesday",
"month":"August",
"time":"21",
"name":"",
"email":"kevin#gmail.com"
},
{
"id":14,
"day":"Saturday",
"month":"December",
"time":"12",
"name":"Sam",
"email":"sam#gmail.com"
},
]
And searchTerm is just a string.
As you can see, there are more fields in the sample data than the one displayed in the HTML but I'm trying only to search for the fields that are displayed in the HTML. Some fields can have null values (for eg. name in the sample data has two null values)
I tried the solutions already provided but none of them are working for my requirement.
P.S: Read somewhere that pipes are not the best option to do functionality like this. I'm ready to implement this logic in the class as well.
Try combining your includes with the logical or-operator (||):
transform(items: any[], terms: string): any[] {
if (!items) return [];
if (!terms) return items;
terms = terms.toLowerCase();
return items.filter(it => {
return it.name.toLowerCase().includes(terms) ||
it.day.toLowerCase().includes(terms) ||
it.month.toLowerCase().includes(terms) ||
it.time.toLowerCase().includes(terms)
});
}
This statement will return true if any of the includes returns true. So basically any item which name, day, month or time contains the searchterm will be returned by the pipe.
This solution assumes that name, day, month and time are not null or undefined. But I'm assuming that is okay as your sample data suggests null values will be empty strings(""). If my assumption is not correct you'll have to check if the values are assigned, before accessing them.
Try this code.. it's pretty simple.
transform(items: any[], terms: string): any[] {
if (!items) return [];
if (!terms) return items;
terms = terms.toLowerCase();
terms = terms.trim();
return items.filter(it => {
if (it.day) {
return it.day.toLowerCase().includes(terms);
}
if (it.month) {
return it.month.toLowerCase().includes(terms);
}
if (it.time) {
return it.time.toLowerCase().includes(terms);
}
if (it.name) {
return it.name.toLowerCase().includes(terms);
}
});
}
If your JSON has null values, you can replace it with an empty string using the following code:
items = JSON.parse(JSON.stringify(items).replace(/null/g, '""'));
This dont work because the includes dont test for multiple term cases. You didnt say whats inside the items Array but if it is a String you could do this:
transform(items: any[], terms: string): any[] {
if(!items) return [];
if(!terms) return items;
terms = terms.toLowerCase();
return items.filter( it => {
//if it is something like "Programmer 07 November 12:00PM"
var informations = it.split(' '); //["Programmer", "07" ,"November" ,"12:00PM"]
var termArray = terms.split(' ');
var rightResult = true;
for (var index in termArray) {
if !(informations.include(termArray[index])) {
rightResult = false;
}
return rightResult;
});
}
Inside your transform method of your search pipe, apply filters on all the fields you want to apply filter on. Following will search for all keys in the object:
transform(items: any[], terms: string): any[] {
if(!items) return [];
if(!terms) return items;
terms = terms.toLowerCase();
return items.filter( it => {
return keys(it).reduce((prev, key) => {
return prev || key.toLowerCase().includes(term);
}, false);
});
}
If keys or Object.keys are not working, use the following code instead of reduce function:
...
let bInclude = false;
for(let key in it){
bInclude = bInclude || key.toLowerCase().includes(term);
}
return bInclude;
...
filterItems(param: any): void {
let val: string = param;
if (val) {
if (val.trim() !== '') {
this.filterItemsList = this.items.filter((data) => {
console.log(data.category);
return data.category.toLowerCase().indexOf(val.toLowerCase()) > -1
|| data.products.some(product => product.code.toLowerCase().indexOf(val.toLowerCase()) > -1);
})
}
}
}
it's working perfect
I added autocomplete field on my form in order to select a patient.
The patient data come from database. The problem is there are 40.000 patients
So i would like to load data after user has entered 3 characters minimum.
But i don't know how to check that and how to pass the input to the function (filter argument).
This is what i have done. for the moment the data are loaded when i click on the input field :
HTML :
<mat-form-field class="index-full-width">
<input
matInput
type="text"
[(ngModel)]="patientChoice"
placeholder="Patient"
aria-label="Patient"
[matAutocomplete]="autoPatient"
[formControl]="myControl"
(click)="getPatients()">
<mat-autocomplete (optionSelected)="selectPat()" #autoPatient="matAutocomplete" [displayWith]="displayFnPat">
<mat-option *ngFor="let patient of filteredPatients | async" [value]="patient">
<span>{{ patient.lastName }}</span>
<small>{{patient.firstName}}</small> |
<span>né(e) le {{ patient.dateNaissance }}</span> |
<small>IPP: {{patient.ipp}}</small>
</mat-option>
</mat-autocomplete>
</mat-form-field>
TS :
getPatients() {
let searchTerm = '*';
let success: any = {};
this.klinckServices.getPatients(searchTerm)
.then((webScriptdata) => {
success = webScriptdata;
this.listPatients = success.data.items ;
console.log(this.listPatients);
},
msg => {
alert(msg);
});
}
ngOnInit() {
this.filteredPatients = this.myControl.valueChanges.pipe(
startWith<string | Patient>(''),
map(patient => typeof patient === 'string' ? patient : patient.name),
map(name => name ? this.filterPatient(name) : this.listPatients.slice())
);
}
displayFnPat(patient: Patient): string | undefined {
return patient ? patient.name : undefined;
}
filterPatient(name: string) {
return this.listPatients.filter(patient =>
patient.name.toLowerCase().includes(name.toLowerCase()));
}
There's another way to do it and it is recommended here.
It's basically checking the length on your filter method.
filterPatient(name: string) {
if (name.length < 2) {
return [];
}
return this.listPatients.filter(patient =>
patient.name.toLowerCase().includes(name.toLowerCase()));
}
OK, solved by adding in HTML (keyup)="getPatients($event)" :
<input
matInput
type="text"
[(ngModel)]="patientChoice"
placeholder="Patient"
aria-label="Patient"
[matAutocomplete]="autoPatient"
[formControl]="myControl"
(keyup)="getPatients($event)"
>
And in TS file :
getPatients(event: any) {
let searchTerm = '';
searchTerm += event.target.value;
console.log(searchTerm);
if (searchTerm.length === 2) {
let success: any = {};
this.klinckServices.getPatients(searchTerm)
.then((webScriptdata) => {
success = webScriptdata;
this.listPatients = success.data.items;
console.log(this.listPatients);
},
msg => {
alert(msg);
});
}
}
Briefly: Is there any way to implement a custom comparator for the number column filters?
Long story:
I use ag-grid in Angular (2). I provide my own components for cells in my ag-grid:
let column = {
headerName: column.header,
field: column.id,
cellRendererFramework: DynamicComponent,
filter: this.convertFormatToFilterType(column.format) // returns "text", "number" or "date"
}
In order to make the filters get values from my cells properly I provide custom comparators (this one is for the text column filters):
if (c.filter === "text")
c['filterParams'] = {
textCustomComparator: (filter, cell, filterText): boolean => {
var filterTextLowerCase = filterText.toLowerCase();
var valueLowerCase = cell.value.toString().toLowerCase();
switch (filter) {
case 'contains':
return valueLowerCase.indexOf(filterTextLowerCase) >= 0;
case 'notContains':
return valueLowerCase.indexOf(filterTextLowerCase) === -1;
case 'equals':
return valueLowerCase === filterTextLowerCase;
case 'notEqual':
return valueLowerCase != filterTextLowerCase;
case 'startsWith':
return valueLowerCase.indexOf(filterTextLowerCase) === 0;
case 'endsWith':
var index = valueLowerCase.lastIndexOf(filterTextLowerCase);
return index >= 0 && index === (valueLowerCase.length - filterTextLowerCase.length);
default:
// should never happen
console.warn('invalid filter type ' + filter);
return false;
}
}
};
You can see I need to access the value of the cell by using "cell.value". The code above works fine.
What I have troubles with is providing similar functionality for the number column filters - they don't seem to use any custom comparator. Therefore, what is happening, the filter tries to access the cell's value directly instead of using "cell.value".
So, is there any way to implement a custom comparator for the number column filters? Or, if not, any other way I can get the value from my cells correctly in this case?
What I ended up doing is implementing a ag-grid custom filter component
import { Component, ViewChild, ViewContainerRef } from '#angular/core';
import { IFilterParams, IDoesFilterPassParams, RowNode, IAfterGuiAttachedParams } from 'ag-grid/main';
import { IFilterAngularComp } from 'ag-grid-angular/main';
// https://www.ag-grid.com/javascript-grid-filter-component/#gsc.tab=0 / Angular Filtering
// create your filter as a Angular component
#Component({
selector: 'filter-cell',
template: `
<select #select (ngModelChange)="onSelectChange($event)" [ngModel]="operator">
<option value="eq">Equals</option>
<option value="neq">Not equal</option>
<option value="lt">Less than</option>
<option value="lte">Less than or equals</option>
<option value="gt">Greater than</option>
<option value="gte">Greater than or equals</option>
<option value="inrange">In range</option>
</select>
<br>
<input #input (ngModelChange)="onChange($event)" [ngModel]="text">
<br>
<div *ngIf='operator === "inrange"'>
<input #input2 (ngModelChange)="onChange2($event)" [ngModel]="text2">
</div>
`,
styles: ['select { margin: 2px 4px; }', 'input { height: 26px; margin: 2px 4px; }']
})
export class GridNumberFilterComponent implements IFilterAngularComp {
private params: IFilterParams;
private valueGetter: (rowNode: RowNode) => any;
public operator: string = 'eq';
public text: string = '';
public text2: string = '';
#ViewChild('select', { read: ViewContainerRef }) public select;
#ViewChild('input', { read: ViewContainerRef }) public input;
#ViewChild('input2', { read: ViewContainerRef }) public input2;
agInit(params: IFilterParams): void {
this.params = params;
this.valueGetter = params.valueGetter;
}
isFilterActive(): boolean {
return this.text !== null && this.text !== undefined && this.text !== '';
}
doesFilterPass(params: IDoesFilterPassParams): boolean {
let cellNumber = Number(this.valueGetter(params.node).value);
let filterNumber = this.text ? Number(this.text) : -Infinity;
let filterNumber2 = this.text2 ? Number(this.text2) : Infinity;
switch (this.operator) {
case 'eq': return cellNumber === filterNumber;
case 'neq': return cellNumber !== filterNumber;
case 'lt': return cellNumber < filterNumber;
case 'lte': return cellNumber <= filterNumber;
case 'gt': return cellNumber > filterNumber;
case 'gte': return cellNumber >= filterNumber;
case 'inrange': return cellNumber >= filterNumber && cellNumber <= filterNumber2;
default: return true;
}
}
getModel(): any {
return { value: this.text };
}
setModel(model: any): void {
this.text = model ? model.value : '';
}
afterGuiAttached(params: IAfterGuiAttachedParams): void {
this.input.element.nativeElement.focus();
}
componentMethod(message: string): void {
alert(`Alert from PartialMatchFilterComponent ${message}`);
}
onSelectChange(newValue): void {
if (this.operator !== newValue) {
this.operator = newValue;
this.params.filterChangedCallback();
}
}
onChange(newValue): void {
if (this.text !== newValue) {
this.text = newValue;
this.params.filterChangedCallback();
}
}
onChange2(newValue): void {
if (this.text2 !== newValue) {
this.text2 = newValue;
this.params.filterChangedCallback();
}
}
}
which I add to my column like this:
let column = {
headerName: column.header,
field: column.id,
cellRendererFramework: DynamicComponent,
filterFramework: GridNumberFilterComponent
}
I am using a form validation with javascript.
When submitting, the background-color of those input fields which are not valid changes to red color. When filling up this field and typing into another input field, the red background-color of the former field should go away. This is at the moment not the case. It only disappears when submitting again. How can I make this possible that the bg color changes back to normal when typing into another field?
// Return true if the input value is not empty
function isNotEmpty(inputId, errorMsg) {
var inputElement = document.getElementById(inputId);
var errorElement = document.getElementById(inputId + "Error");
var inputValue = inputElement.value.trim();
var isValid = (inputValue.length !== 0); // boolean
showMessage(isValid, inputElement, errorMsg, errorElement);
return isValid;
}
/* If "isValid" is false, print the errorMsg; else, reset to normal display.
* The errorMsg shall be displayed on errorElement if it exists;
* otherwise via an alert().
*/
function showMessage(isValid, inputElement, errorMsg, errorElement) {
if (!isValid) {
// Put up error message on errorElement or via alert()
if (errorElement !== null) {
errorElement.innerHTML = errorMsg;
} else {
alert(errorMsg);
}
// Change "class" of inputElement, so that CSS displays differently
if (inputElement !== null) {
inputElement.className = "error";
inputElement.focus();
}
} else {
// Reset to normal display
if (errorElement !== null) {
errorElement.innerHTML = "";
}
if (inputElement !== null) {
inputElement.className = "";
}
}
}
The form:
<td>Name<span class="red">*</span></td>
<td><input type="text" id="name" name="firstname"/></td>
<p id="nameError" class="red"> </p>
The submit:
<input type="submit" value="SEND" id="submit"/>
Css:
input.error { /* for the error input text fields */
background-color: #fbc0c0;
}
Update:
I tried this but it seems not to work:
function checkFilled() {
var inputVal = document.querySelectorAll("#offerteFirstname, #offerteLastname, #offertePhone, #offertePoster, #offerteStreet").value;
if (inputVal == "") {
document.querySelectorAll("#offerteFirstname, #offerteLastname, #offertePhone, #offertePoster, #offerteStreet").style.backgroundColor = "red";
}
else{
document.querySelectorAll("#offerteFirstname, #offerteLastname, #offertePhone, #offertePoster, #offerteStreet").style.backgroundColor = "white";
}
}
checkFilled();
here example of it
<input type="text" id="subEmail" onchange="checkFilled();"/>
and now you can JavaScript on input
function checkFilled() {
var inputVal = document.getElementById("subEmail").value;
if (inputVal == "") {
document.getElementById("subEmail").style.backgroundColor = "white";
}
else{
document.getElementById("subEmail").style.backgroundColor = "red";
}
}
checkFilled();
here working demo
Try
$(formElement).on('keyup','input.error', function(){
if(<values changed>){
$(this).removeClass('error');
}
});
In Chosen drop down plugin, a search is started after 2 characters are typed in chosen drop down.
I need the search to not start until after inputing at least two characters in the search box.
Can any one suggest how to do this?
I did a small change to start to search after the third character, is not the best option but works, in the chosen JS in the AbstractChosen.prototype.winnow_results function after the line searchText = this.get_search_text(); add the following code: if (searchText != "" && searchText.length < 3) return;. Remember to change the < 3 by your own size.
Hope this help you
See part of the code below:
AbstractChosen.prototype.winnow_results = function() {
var escapedSearchText, option, regex, regexAnchor, results, results_group, searchText, startpos, text, zregex, _i, _len, _ref;
this.no_results_clear();
results = 0;
searchText = this.get_search_text();
if (searchText != "" && searchText.length < 3) return;
escapedSearchText = searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
I know this post it's old but i had to face this problem just now and wanted to share my result. I wrapped everything in a function extendedSearch and set it as a callback while chosen is emitting the chosen:showing_dropdown event.
My problem was that i need the search to show the results after the 2nd character typed in search and must filter out certain strings from the results.
Bellow you'll find a demo which shows results on the 3rd character typed in, and will keep visible only those results that end with letter "E".
$(function() {
/**
* By default the results are hidden when clicking a dropdown.
* {
* toggleResults: false, // a function(searchValue) that returns a boolean
* filterResults: false, // a function(dropDownValue, selectValue) that returns a boolean
* }
* #param options
*/
const extendedSearch = (options = {}) => {
const defaultOptions = {
toggleResults: false,
filterResults: false,
};
options = { ...{},
...defaultOptions,
...options
};
/**
* Main element
*/
return (evt, params) => {
let originalElement = $(evt.currentTarget);
let searchInput = params.chosen.search_field;
const customSearch = (options = {}) => {
let defaultOptions = {
originalElement: null,
searchInput: null
};
options = { ...{},
...defaultOptions,
...options
};
if (!(options.originalElement instanceof jQuery) || !options.originalElement) {
throw new Error('Custom Search: originalElement is invalid.');
}
if (!(options.searchInput instanceof jQuery) || !options.searchInput) {
throw new Error('Custom Search: searchInput is invalid.');
}
let res = options.searchInput
.parent()
.next('.chosen-results');
res.hide();
if (typeof options.toggleResults !== 'function') {
options.toggleResults = (value) => true;
}
if (options.filterResults && typeof options.filterResults !== 'function') {
options.filterResults = (shownText = '', selectValue = '') => true;
}
/**
* Search Input Element
*/
return (e) => {
let elem = $(e.currentTarget);
let value = elem.val() || '';
if (value.length && options.toggleResults(value) === true) {
res.show();
if (options.filterResults) {
let children = res.children();
let active = 0;
$.each(children, (idx, item) => {
let elItem = $(item);
let elemIdx = elItem.attr('data-option-array-index');
let shownText = elItem.text();
let selectValue = options.originalElement.find('option:eq(' + elemIdx + ')').attr('value') || '';
if (options.filterResults(shownText, selectValue) === true) {
active++;
elItem.show();
} else {
active--;
elItem.hide();
}
});
if (active >= 0) {
res.show();
} else {
res.hide();
}
}
} else {
res.hide();
}
};
};
options = {
...{},
...options,
...{
originalElement,
searchInput
}
};
let searchInstance = customSearch(options);
searchInput
.off('keyup', searchInstance)
.on('keyup', searchInstance)
}
};
/** This is the final code */
const inputValidator = (value) => {
console.log('input', value);
return $.trim(value).length > 2;
};
const textResultsValidator = (dropDownValue, selectValue) => {
if ($.trim(dropDownValue).substr(-1, 1) === 'E') {
console.log('results shown', dropDownValue, '|', selectValue);
return true;
}
return false;
};
$(".chosen-select")
.chosen()
.on('chosen:showing_dropdown', extendedSearch({
toggleResults: inputValidator,
filterResults: textResultsValidator
}));
});
#import url("https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.min.css")
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.jquery.min.js"></script>
<select class="chosen-select">
<option value="">Pick something</option>
<option value="APPLE">APPLE</option>
<option value="APPLE JUICE">APPLE JUICE</option>
<option value="BANANA">BANANA</option>
<option value="ANANAS">ANANAS</option>
<option value="ORANGE">ORANGE</option>
<option value="ORANGES">ORANGES</option>
<option value="STRAWBERRY">STRAYBERRY</option>
</select>