Angular 2 Prevent input and model changing using directive - javascript

I need to validate a field to only have numbers in a range. So I have tried to write a directive:
#Directive({
selector: "[input-limitation]",
host: {
'(input)': 'onChange($event)',
}
})
export class InputLimitationDirective {
#Input("input-limitation") settings: InputLimitationSettings;
public constructor(private el: ElementRef) {
let self = this;
console.log(":::::::::::::::: el.nativeElement", el.nativeElement);
//jQuery(el.nativeElement).on('keypress', function (e) { self.onChange(e) });
};
private onChange($event) {
console.log("InputLimitationDirective", this.settings);
if (this.settings.InputType = "number") {
return this.numberLimitation($event);
}
}
private numberLimitation($event: any) {
let val: number = $event.target.value;
console.log("InputLimitationDirective", val);
console.log(val, this.settings.MinValue);
console.log(!val, val*1 < this.settings.MinValue*1);
if (!val || val*1 <= this.settings.MinValue*1) {
console.log("1 case");
event.preventDefault();
event.stopPropagation();
return false;
}
else if (val*1 >= this.settings.MaxValue*1) {
console.log("2 case");
event.preventDefault();
event.stopPropagation();
return false;
};
return true;
}
}
And use in in this way:
<input (change)="totalAmountChanged($event.target.value)"
[(ngModel)]="model.UsedVolume"
[disabled]="!isEditMode"
type="number"
[input-limitation]="usedVolumeInputLimitationsSettings"
pattern="^[1-9]\d*$" min="1" max="999" maxlength="3"
class="form-control length-3-input" />
But it is some big problem:
1. This limitation fired AFTER Angular 2 model changed, so I will get 0 value in model (but I need 1 as a min value).
2. This just change input value, but not Angular 2 model value.
So, is it possible to validate and prevent some inputs before Angular model changed?

I should use a custom value accessor to implement this. Here is a sample:
const CUSTOM_VALUE_ACCESSOR = new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => MaxValueAccessor), multi: true});
#Directive ({
selector: 'input[max]',
host: { '(input)': 'doOnChange($event.target)' },
providers: [ CUSTOM_VALUE_ACCESSOR ]
})
export class MaxValueAccessor extends DefaultValueAccessor {
onChange = (_) => {};
onTouched = () => {};
writeValue(value:any):void {
if (value!=null) {
super.writeValue(value);
}
}
doOnChange(elt) {
var val = elt.value;
// check value here
elt.value = val;
this.onChange(val);
}
}
This way you can plug into the ngModel handling.
See this question for more details:
Can't update bind value on blur or on setValue

Related

Why does my form input only register one character after re-render?

On initial render, the input components acts as normal. However after it is unfocused and refocused again, it will only accept one character. This also happens if I reset the form.
I created a FormValidationStateManager class to reuse Joi validation logic that is needed.
Form.jsx
class Form extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.validation = new FormValidationStateManager();
}
handleSubmit(e) {
e.preventDefault();
var result = this.validation.validateAll();
this.setState({
// need to do this, trigger render after validations
// we could eventually encapsulate this logic into FormValidationSM.
submit: Date.now()
}, function() {
if (result.valid) {
this.props.onSubmit(result.data);
}
}.bind(this));
}
resetForm(e) {
e.preventDefault();
this.validation.clearForm();
this.setState({
// need to do this, trigger render after validations,
// we could eventually encapsulate this logic into FormValidationSM.
submit: Date.now()
});
}
render() {
// TODO implement logic for rendering name from API call
let name = "John Doe";
return (
<form class='info-form' onSubmit={this.handleSubmit} autocomplete='off'>
<div class='info-block-section-title'>
<h2>Welcome {name}</h2>
<h4>Some nicely worded few sentences that let's the user know they just need to fill out a few bits of information to start the process of getting their student loans paid down!</h4>
</div>
<br />
<div class='info-block-section-body'>
<InfoFieldset validation={this.validation} />
</div>
<div class='info-form-button'>
<FormButton label="Continue" />
<FormButton type="button" onClick={this.resetForm.bind(this)} label="Reset" />
</div>
</form>
)
}
}
export default Form;
Fieldset
class Fieldset extends Component {
constructor(props) {
super(props);
this.state = {
secondaryEmailVisible: false
};
this.handleToggle = this.handleToggle.bind(this);
this.validation = this.props.validation || new FormValidationStateManager();
this.validation.addRules(this.validatorRules.bind(this));
}
validatorRules() {
var _rules = {
// TODO validate format of date, address and zip
date_of_birth: Joi.date().label("Date of Birth").required(),
address_street: Joi.string().label("Street Address").required(),
address_zip: Joi.string().label("Zip Code").required(),
password_confirmation: Joi.any()
.valid(Joi.ref('password'))
.required()
.label('Password Confirmation')
.messages({
"any.only": "{{#label}} must match password"
}).strip(),
password: Joi.string()
.pattern(/\d/, 'digit')
.pattern(/^\S*$/, 'spaces')
.pattern(/^(?!.*?(.)\1{2})/, 'duplicates')
.pattern(/[a-zA-z]/, 'alpha')
.required()
.min(CONSTANTS.PASSWORD_MIN)
.label('Password')
.messages({
"string.min": PASSWORD_HINT,
"string.pattern.name": PASSWORD_HINT
})
}
if (this.state.secondaryEmailVisible) {
_rules['secondary_email'] = Joi.string()
.label("Secondary Email")
.email({ tlds: false })
.required();
}
return _rules;
}
handleToggle() {
this.setState(state => ({
secondaryEmailVisible: !state.secondaryEmailVisible
}));
}
render() {
return(
<div class='info-fieldset'>
{/* for skipping chrome browser autocomplete feature
<input type='text' style={{ display: 'none '}} />
<input type='password' style={{ display: 'none '}} />
*/ }
<strong>Primary email for login: work#example.com</strong>
<FormElementToggle
field="secondary"
label={"Add another email for notifications?"}
onChange={this.handleToggle} />
<div class='info-form-element'>
<DisplayToggle remove={true} show={this.state.secondaryEmailVisible}>
<FormElementInput
field='secondary_email'
label="Secondary Email"
validation={this.validation}
placeholder='john#example.com' />
</DisplayToggle>
<FormElementInput
field='password'
type='password'
label={(
<div>
Password
<Tooltip
placement='top'
trigger={['hover']}
overlay={PASSWORD_HINT}>
<span><i className='fa fa-info-circle'></i></span>
</Tooltip>
</div>
)}
placeholder='********'
validation={this.validation} />
<FormElementInput
key={1}
field='password_confirmation'
type='password'
label="Password Confirmation"
placeholder='********'
validation={this.validation} />
<FormElementInput
field='date_of_birth'
label="Date of Birth"
validation={this.validation}
placeholder='MM/DD/YYYY' />
<FormElementInput
field='address_street'
label="Street Address"
validation={this.validation}
placeholder='123 Example St' />
<FormElementInput
field="address_zip"
label="Zip Code"
validation={this.validation}
placeholder='01234' />
</div>
</div>
)
}
}
export default Fieldset;
FormElementInput.jsx
class FormElementInput extends Component {
constructor(props) {
super(props);
this.input = React.createRef();
this.cachedErrors = {};
this.cachedValue = null;
this.state = { focused: false };
this.validation = this.props.validation || new FormValidationStateManager();
}
componentDidUpdate(prevProps, prevState) {
}
shouldComponentUpdate(nextProps, nextState) {
var filter = function(val, key, obj) {
if (_.isFunction(val)) {
return true;
}
// Certain props like "label" can take a React element, but those
// are created dynamically on the fly and they have an random internal UUID,
// which trips the deep-equality comparision with a false-positive.
// This is wasted cycles for rendering.
if (React.isValidElement(val)) {
return true;
}
// validation is a complex obj that we shouldn't be caring about
if (_.contains(['validation'], key)) {
return true;
}
};
// if the errors after a validation got updated, we should re-render.
// We have to compare in this manner because the validation object is not shallow-copied
// via props since it is managed at a higher-component level, so we need to
// do a local cache comparision with our local copy of errors.
if (!_.isEqual(this.cachedErrors, this.getErrors())) {
return true;
}
// if the errors after a validation got updated, we should re-render.
// We have to compare in this manner because the validation object is not shallow-copied
// via props since it is managed at a higher-component level, so we need to
// do a local cache comparision with our local copy of errors.
if (this.cachedValue !== this.getDisplayValue()) {
return true;
}
return !_.isEqual(_.omit(nextProps, filter), _.omit(this.props, filter)) || !_.isEqual(this.state, nextState);
}
setValue(value) {
console.error('deprecated - can not set value directly on FormElementInput anymore');
throw 'deprecated - can not set value directly on FormElementInput anymore';
}
isDollarType() {
return this.props.type === 'dollar';
}
isRateType() {
return this.props.type === 'rate';
}
getType() {
if (this.isDollarType() || this.isRateType()) {
return 'text';
} else {
return this.props.type;
}
}
getPrefix() {
if (this.isDollarType()) {
return _.compact([this.props.prefix, '$']).join(' ');
} else {
return this.props.prefix;
}
}
getPostfix() {
if (this.isRateType()) {
return _.compact([this.props.postfix, '%']).join(' ');
} else {
return this.props.postfix;
}
}
getDisplayValue() {
var value = this.props.value || this.validation.getFormValue(this.props.field);
// while DOM input is focused, just return the same value being typed
if (this.state.focused) {
return value;
}
if (this.isDollarType()) {
if (_.isUndefined(value)) {
return value;
}
// keep in sync with the validation check in getOnChange()
// even if this is different - it wont break - cause accounting.js handles gracefully
else if (_.isNumber(value) || !value.match(/[a-z]/i)) {
return accounting.formatMoney(value, '', 2);
} else {
return value;
}
} else if (this.isRateType()){
if (_.isUndefined(value)) {
return '';
}
return accounting.formatNumber(value, 3)
} else {
return value;
}
}
getErrors() {
return this.props.errors || this.validation.getValidationMessages(this.props.field);
}
onBlur(event) {
this.validation.validate(this.props.field);
this.setState({ focused: false });
}
onFocus(event) {
this.setState({ focused: true });
}
onChange(event) {
if (this.isDollarType()) {
// only accept if the input at least resembles a currency
// accounting.parse already strips alot of characters and does this silently
// we want to be a little less strict than accounting.parse since we need to allow more
// different types of inputs +-,. and also let accounting.parse do its job
// very basic check here for a-z characters
if (!event.target.value.match(/[a-z]/i)) {
var parsedValue = accounting.parse(event.target.value);
event._parsedValue = parsedValue;
}
}
let _value = event._parsedValue || event.target.value;
console.log(this.input)
this.validation.setFormValue(this.props.field, _value);
}
_eventHandlers(field) {
return {
onChange: this.onChange.bind(this),
onBlur: this.onBlur.bind(this),
onFocus: this.onFocus.bind(this)
};
}
_mergeEventHandlers(p1, p2) {
var events = [
'onChange',
'onBlur',
'onFocus'
];
var r1 = {};
events.map(function(e) {
r1[e] = FuncUtils.mergeFunctions(p1[e], p2[e]);
}.bind(this));
return r1;
}
render() {
let _errors = this.cachedErrors = this.getErrors();
let _value = this.cachedValue = this.getDisplayValue();
let attrs = {
id: this.props.htmlId || ('field_' + this.props.field),
className: classnames({
'error': _errors && _errors.length
}),
type: this.getType(),
placeholder: this.props.placeholder,
autoComplete: "false",
disabled: this.props.disabled,
readOnly: this.props.disabled,
value: _value,
ref: this.input,
};
attrs = _.extend(attrs, this._mergeEventHandlers(this.props, this._eventHandlers()));
const prefix = this.getPrefix();
const postfix = this.getPostfix();
return (
<FormElementWrapper
type="input"
field={this.props.field}
errors={_errors}
label={this.props.label}>
<div className="form-element-input">
<DisplayToggle remove={true} hide={!prefix}>
<span className="prefix">{prefix}</span>
</DisplayToggle>
<div className={classnames({
"has-prefix": !!prefix,
"has-postfix": !!postfix
})}>
<input {...attrs} />
</div>
<DisplayToggle remove={true} hide={!postfix}>
<span className="postfix">{postfix}</span>
</DisplayToggle>
</div>
<div className="form-error">
{_errors.map((msg, idx) => {
return (<FormError key={idx}>{msg}</FormError>)
})}
</div>
</FormElementWrapper>
)
}
};
FormElementInput.PropTypes = {
field: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
value: PropTypes.any,
label: PropTypes.any,
errors: PropTypes.array,
type: PropTypes.string,
placeholder: PropTypes.string,
onChange: PropTypes.func,
onFocus: PropTypes.func,
onKeyUp: PropTypes.func,
tooltip: PropTypes.string,
prefix: PropTypes.string,
postfix: PropTypes.string,
disabled: PropTypes.bool,
disableTrimming: PropTypes.bool
}
FormElementInput.defaultProps = {
type: 'text',
placeholder: 'Enter Value',
tooltip: undefined,
prefix: undefined,
postfix: undefined,
disabled: false,
disableTrimming: false
}
export default FormElementInput;
FormValidationStateManager.js
class FormValidationStateManager {
// default state, validation ref obj
constructor(defaultState) {
this.rules = [];
this.state = defaultState || {};
this.errors = {};
this.dirty = false;
}
getFormValue(key) {
return this.state[key];
}
setFormValue(key, value) {
this.dirty = true;
this.state[key] = value;
}
setFormState(newState) {
this.state = _.extend({}, newState);
}
addRules(rules) {
this.rules.push(rules);
}
isFormDirty(){
return this.dirty;
}
/**
* Clear all previous validations
*
* #return {void}
*/
clearValidations() {
this.dirty = false;
this.errors = {};
}
clearForm() {
this.state = _.mapObject(this.state, function(v, k) {
return '';
});
this.clearValidations();
}
/**
* Check current validity for a specified key or entire form.
*
* #param {?String} key to check validity (entire form if undefined).
* #return {Boolean}.
*/
isValid(key) {
return _.isEmpty(this.getValidationMessages(key));
}
// call within submit
validateAll() {
return this.validate();
}
/**
* Method to validate single form key or entire form against the component data.
*
* #param {String|Function} key to validate, or error-first containing the validation errors if any.
* #param {?Function} error-first callback containing the validation errors if any.
*/
validate(key, callback) {
if (_.isFunction(key)) {
callback = key;
key = undefined;
}
var schema = this._buildRules();
var data = this.state;
var result = this._joiValidate(schema, data, key);
if (key) {
this.errors[key] = result.errors[key];
} else {
this.errors = result.errors;
}
return {
valid: this.isValid(key),
errors: this.errors,
data: result.data
};
}
_buildRules() {
var allRules = this.rules.map(function(r) {
// Rules can be functions and be dynamically evalulated on-the-fly,
// to allow for more complex validation rules.
if (_.isFunction(r)) {
return r();
} else {
return r;
}
});
// collapse all the rules into one single object that will
// be evaluated against `this.state`
return Joi.object(_.extend({}, ...allRules));
}
/**
* Get current validation messages for a specified key or entire form.
*
* #param {?String} key to get messages, or entire form if key is undefined.
* #return {Array}
*/
getValidationMessages(key) {
let errors = this.errors || {};
if (_.isEmpty(errors)) {
return [];
} else {
if (key === undefined) {
return _.flatten(_.keys(errors).map(error => {
return errors[error] || []
}));
} else {
return errors[key] ? errors[key].map(he.decode) : [];
}
}
}
_joiValidate(joiSchema, data, key) {
joiSchema = joiSchema || Joi.object();
data = data || {};
const joiOptions = {
// when true, stops validation on the first error, otherwise returns all the errors found. Defaults to true.
abortEarly: false,
// when true, allows object to contain unknown keys which are ignored. Defaults to false.
allowUnknown: true,
// remove unknown elements from objects and arrays. Defaults to false
stripUnknown: true,
errors: {
escapeHtml: true,
// overrides the way values are wrapped (e.g. [] around arrays, "" around labels).
// Each key can be set to a string with one (same character before and after the value)
// or two characters (first character before and second character after), or false to disable wrapping:
label: false,
wrap: {
label: false,
array: false
}
},
messages: {
"string.empty": "{{#label}} is required",
"any.empty": "{{#label}} is required",
"any.required": "{{#label}} is required"
}
};
const result = joiSchema.validate(data, joiOptions);
let errors = this._formatErrors(result);
if (key) {
errors = _.pick(errors, key);
}
return {
errors: errors,
data: result.value
};
}
_formatErrors(joiResult) {
if (joiResult.error) {
return _.reduce(joiResult.error.details, (memo, detail) => {
// According to docs:
// - "detail.path": ordered array where each element is the accessor to the value where the error happened.
// - "detail.context.key": key of the value that erred, equivalent to the last element of details.path.
// Which is why we get the last element in "path"
var key = _.last(detail.path);
if (!Array.isArray(memo[key])) {
memo[key] = [];
}
if (!_.contains(memo[key], detail.message)) {
memo[key].push(detail.message);
}
return memo;
}, {});
} else {
return {};
}
}
}
export default FormValidationStateManager;
I think I got it. I changed value: _value to defaultValue: _value for the attributes in FormElementInput.jsx
saw it here
Anyone know why this fixed it under the hood?

PrimeNG p-table header select all persistence with lazy loading and pagination

Current config (cannot update it to latest):
"#angular/cli": "^7.3.9",
"primeng": "7.0.5",
I have a PrimeNG p-table that has lazy loaded data with pagination.
There is an issue open for it on PrimeNG GitHub too - https://github.com/primefaces/primeng/issues/8139
Stackblitz link is already attached in that issue so didn't create a new one.
Scenario:
One 1st page, some rows are selected via checkbox selection.
On 2nd page, Select All checkbox from the header is selected and all rows on 2nd page is auto-selected.
Now when navigated to the first page, the selections from here are reset. But the Select All checkbox in the header is still checked.
Would like to know if anyone has a workaround for this issue?
Any help is appreciated.
Edit:
Solution found in another similar GitHub issue: https://github.com/primefaces/primeng/issues/6482
Solution:
https://github.com/primefaces/primeng/issues/6482#issuecomment-456644912
Can someone help with the implementation of the override in an Angular 7/8 application. Not able to understand as how to get the TableHeaderCheckbox reference and override the prototype.
Well, the solution to the problem is still not added to the PrimeNG repo and so even the latest package does not have it solved.
For time being, use the solution mentioned in the question under Edit
To answer the question that I have asked under the Edit, check below:
// In some service file:
import { Table, TableHeaderCheckbox } from 'primeng/table';
import { ObjectUtils } from 'primeng/components/utils/objectutils';
import { uniq, each, intersection, map, remove } from 'lodash';
#Injectable()
export class BulkSelectAllPagesService {
overridePrimeNGTableMethods() {
TableHeaderCheckbox.prototype.updateCheckedState = function () {
const currentRows = map(this.dt.value, this.dt.dataKey);
const selectedRows = map(this.dt.selection, this.dt.dataKey);
this.rowsPerPageValue = this.dt.rows;
const commonRows = intersection(currentRows, selectedRows);
return commonRows.length === currentRows.length;
};
Table.prototype.toggleRowsWithCheckbox = function (event, check) {
let _selection;
if (!check) {
_selection = this.value.slice();
each(_selection, (row) => {
const match = {}; match[this.dataKey] = row[this.dataKey];
remove(this._selection, match);
});
} else {
_selection = check ? this.filteredValue ? this.filteredValue.slice() : this.value.slice() : [];
each(this._selection, (row) => {
const match = {}; match[this.dataKey] = row[this.dataKey];
remove(_selection, match);
});
this._selection = this._selection.concat(_selection);
}
this.preventSelectionSetterPropagation = true;
this.updateSelectionKeys();
this.selectionChange.emit(this._selection);
this.tableService.onSelectionChange();
this.onHeaderCheckboxToggle.emit({
originalEvent: event,
affectedRows: _selection,
checked: check
});
};
}
// In app.component.ts
import { Component, OnInit } from '#angular/core';
import { BulkSelectAllPagesService } from 'PATH_TO_THE_FILE/bulk-select-all-pages.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
constructor(
private bulkSelectAllPagesService: BulkSelectAllPagesService) {
}
ngOnInit() {
this.bulkSelectAllPagesService.overridePrimeNGTableMethods();
}
}
Ofcourse need to include the service file in the providers[] in the app.module.ts
Will create a stackblitz and add later.
Improved version to handle rowspan grouped data:
overridePrimeNGTableMethods() {
TableHeaderCheckbox.prototype.updateCheckedState = function () {
const currentRows = map(this.dt.value, this.dt.dataKey);
const uniqueCurrentRows = uniq(currentRows);
const selectedRows = map(this.dt.selection, this.dt.dataKey);
this.rowsPerPageValue = this.dt.rows;
const commonRows = intersection(currentRows, selectedRows);
if (currentRows.length) {
return commonRows.length === uniqueCurrentRows.length;
} else {
return false;
}
};
Table.prototype.toggleRowWithCheckbox = function (event, rowData) {
const findIndexesInSelection = (selection: any = [], data: any = {}, dataKey: any) => {
const indexes = [];
if (selection && selection.length) {
selection.forEach((sel: any, i: number) => {
if (data[dataKey] === sel[dataKey]) {
indexes.push(i);
}
});
}
return indexes;
};
this.selection = this.selection || [];
const selected = this.isSelected(rowData);
const dataKeyValue = this.dataKey ? String(ObjectUtils.resolveFieldData(rowData, this.dataKey)) : null;
this.preventSelectionSetterPropagation = true;
if (selected) {
const selectionIndexes = findIndexesInSelection(this.selection, rowData, this.dataKey);
const selectedItems = this.selection.filter((val: any) => {
return val[this.dataKey] === rowData[this.dataKey];
});
this._selection = this.selection.filter((val: any, i: number) => {
return selectionIndexes.indexOf(i) === -1;
});
this.selectionChange.emit(this.selection);
selectedItems.forEach((selectedItem: any, index: number) => {
this.onRowUnselect.emit({ originalEvent: event.originalEvent, index: event.rowIndex + index, data: selectedItem, type: 'checkbox' });
});
delete this.selectionKeys[rowData[this.dataKey]];
} else {
let rows = [rowData];
if (dataKeyValue) {
rows = this.value.filter(val => {
return (val[this.dataKey]).toString() === dataKeyValue;
});
}
this._selection = this.selection ? this.selection.concat(rows) : rows;
this.selectionChange.emit(this.selection);
this.onRowSelect.emit({ originalEvent: event.originalEvent, index: event.rowIndex, data: rowData, type: 'checkbox' });
if (dataKeyValue) {
this.selectionKeys[dataKeyValue] = 1;
}
}
this.tableService.onSelectionChange();
if (this.isStateful()) {
this.saveState();
}
};
Table.prototype.toggleRowsWithCheckbox = function (event, check) {
let _selection;
if (!check) {
_selection = this.value.slice();
each(_selection, (row) => {
const match = {}; match[this.dataKey] = row[this.dataKey];
remove(this._selection, match);
});
} else {
_selection = check ? this.filteredValue ? this.filteredValue.slice() : this.value.slice() : [];
each(this._selection, (row) => {
const match = {}; match[this.dataKey] = row[this.dataKey];
remove(_selection, match);
});
this._selection = this._selection.concat(_selection);
}
this.preventSelectionSetterPropagation = true;
this.updateSelectionKeys();
this.selectionChange.emit(this._selection);
this.tableService.onSelectionChange();
this.onHeaderCheckboxToggle.emit({
originalEvent: event,
affectedRows: _selection,
checked: check
});
};
}

how to get the exact search result in angular 4

i have implemented the search module in my app. The search does not search for the exact phrase, but rather individually for each word in the phrase. For example, if you search for "Comprehensive Metabolic", you would only expect to see the CMP Panels, but the search actually returns every single panel that has either the word "Comprehensive" or "Metabolic", which is a much longer list.
any help can i get?
is there any pipe i can use to filter exact search?
here is my search component html
<input #searchInput type="text" (focus)="onFocus($event)" (blur)="onBlur($event)" (keyup)="onKeyUp($event)" placeholder="Search">
its Ts file
#Input() searchTerm: string = "";
#Output() onSearchInputUpdate = new EventEmitter();
#ViewChild("searchInput") searchInputField: ElementRef;
public searchFocus: boolean;
private searchTermTimeoutId;
private waitTime: number = 500; // half a second
onBlur(event) {
// setTimeout so clearSearch click event has time to be called first
setTimeout(() => {
if (event.srcElement.value.length === 0) {
this.searchFocus = false;
}
}, 100);
}
onKeyUp(event) {
if (this.searchTermTimeoutId) {
clearTimeout(this.searchTermTimeoutId);
}
this.searchTermTimeoutId = setTimeout(() => {
this.onSearchInputUpdate.emit(this.searchInputField.nativeElement.value);
}, this.waitTime);
}
i added this in my component where i am using it
here parent component's html
<app-search-list (onSearchInputUpdate)="onSearchFieldUpdate($event)">
</app-search-list>
<app-test-selection-category-list
(onCategorySelect)="updateTestPanelView($event)"></app-test-selection-
category-list>
its Ts File
onSearchFieldUpdate($event) {
this.searchField = $event;
this.updateTestPanelView(this.selectedCategoryId);
}
updateTestPanelView(categoryId: string) {
this.selectedCategoryId = categoryId;
switch (this.selectedCategoryId) {
case '-1':
this.fetchAllTests();
break;
case "0":
this.fetchFavoritesForCategories();
break;
default:
this.fetchTestsForCategory();
}
}
fetchAllTests() {
this.testOrderService.getAllTests(this.searchField).subscribe(response =>
{
const {panels, tests} = this.extractPanelsAndTests(response);
this.testSelectionSession = {
...this.testSelectionSession,
PanelsForAll: panels,
IndividualTestPanelsForAll: tests
};
this.store.dispatch(
new SetTestOrderTestSelectionSession(this.testSelectionSession)
);
})
}
fetchFavoritesForCategories() {
this.testOrderService
.getAllFavorites(this.searchField)
.subscribe(favorites => {
this.testSelectionSession = Object.assign(
{},
this.testSelectionSession,
{
FavoritesByCategory: _.groupBy(favorites, 'CategoryName')
}
);
this.store.dispatch(
new SetTestOrderTestSelectionSession(this.testSelectionSession)
);
});
}
fetchTestsForCategory() {
this.testOrderService
.getTestsByCategoryId(this.selectedCategoryId, this.searchField)
.subscribe(categoryResponse => {
const {panels, tests} = this.extractPanelsAndTests(categoryResponse);
this.testSelectionSession = Object.assign(
{},
this.testSelectionSession,
{
PanelsForCategory: panels.map(panel => {
panel.CategoryId = this.selectedCategoryId;
return panel;
}),
IndividualTestPanelsForCategory: tests.map(
test => {
test.CategoryId = this.selectedCategoryId;
return test;
}
)
}
);
this.store.dispatch(
new SetTestOrderTestSelectionSession(this.testSelectionSession)
);
});
}
i am getting every result which has either Comprehensive or metabolic.
like this
what can i do to get exact result
any help?
Thanks

Unable to access 'this' context in addEventListener() in Angular 2 component

Unable to access 'this' context inside addEventListener() in angular 2
In my rich text editor component, I am implementing image upload functionality.
For that I have written an addEventListener event handler for 'change' in image input tag, I need to access an app service inside event listener via 'this'.
Imageinput.addEventListener('change', () => {
const file = Imageinput.files[0];
var a = this;
if (Imageinput.files != null && Imageinput.files[0] != null) {
debugger;
this._imageUploadService.uploadImageToBlob(file).subscribe((res) => {
debugger;
this._returnedURL = res.imageUrl;
this.pushImageToEditor();
});
}
}
);
but this._imageUploadService returns undefined everytime,even though console don't have any error.
Here is my complete component.ts code -
export class CreateArticleComponent extends AppComponentBase {
#ViewChild("fileInput") fileInput;
public editor;
public _returnedURL = "";
constructor(
injector: Injector,
private _imageUploadService: ArticleServiceProxy,
) {
super(injector);
}
onEditorBlured(quill) {
console.log('editor blur!', quill);
}
onEditorFocused(quill) {
console.log('editor focus!', quill);
}
onEditorCreated(quill) {
console.log(quill);
let toolbar = quill.getModule('toolbar');
toolbar.addHandler('image', this.imageHandler);
//this.editorContent = quill;
this.editor = quill;
console.log('quill is ready! this is current quill instance object', quill);
}
imageHandler() {
debugger;
let self = this;
const Imageinput = document.createElement('input');
Imageinput.setAttribute('type', 'file');
//Imageinput.setAttribute('name', 'articleImage');
Imageinput.setAttribute('accept', 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon');
Imageinput.classList.add('ql-image');
Imageinput.addEventListener('change', () => {
const file = Imageinput.files[0];
if (Imageinput.files != null && Imageinput.files[0] != null) {
debugger;
this._imageUploadService.uploadImageToBlob(file).subscribe((res) => {
debugger;
this._returnedURL = res.imageUrl;
this.pushImageToEditor();
});
}
}
);
Imageinput.click();
}
SendFileToServer(file: any) {
debugger;
this._imageUploadService.uploadImageToBlob(file).subscribe((res) => {
debugger;
this._returnedURL = res.imageUrl;
this.pushImageToEditor();
});
}
pushImageToEditor() {
debugger;
const range = this.editor.getSelection(true);
const index = range.index + range.length;
this.editor.insertEmbed(range.index, 'image', this._returnedURL);
}
ngAfterViewInit() {
}
}
Here is my editor HTML -
<quill-editor #fileInput
[(ngModel)]="editorContent"
[ngModelOptions]="{standalone: true}"
(onEditorCreated)="onEditorCreated($event)"
(onContentChanged)="onContentChanged($event)"
(onSelectionChanged)="logSelection($event)"
[style]="{'height':'300px'}">
</quill-editor>
I can access this._imageUploadService in other methods but not able to access it inside addEventListener().Any help will be appreciated
In change event handler this refers to toolbar.addHandler's context, so you need it to bind this.imageHandler like this
onEditorCreated(quill) {
console.log(quill);
let toolbar = quill.getModule('toolbar');
toolbar.addHandler('image', this.imageHandler.bind(this)); // <--
//this.editorContent = quill;
this.editor = quill;
console.log('quill is ready! this is current quill instance object', quill);
}
instead of using .bind or trying to force objects/context into callbacks you can simply use angular Renderer2 wrapper that will give you access to the Component class context.
imageHandler() {
debugger;
const Imageinput = this.renderer.createElement('input');
this.renderer.setAttribute(Imageinput, 'type', 'file');
this.renderer.setAttribute(Imageinput, 'accept', 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon');
this.renderer.addClass(Imageinput, 'ql-image');
this.renderer.listen(Imageinput, 'change', () => {
if (Imageinput.files != null && Imageinput.files[0] != null) {
debugger;
this._imageUploadService.uploadImageToBlob(file).subscribe((res) => {
debugger;
this._returnedURL = res.imageUrl;
this.pushImageToEditor();
});
}
});
Imageinput.click();
}

Angular 4 - Custom validator with dynamic parameter value

I have written a custom validator that checks if a date is above a certain minimum date.
the code looks like this:
export function validateMinDate(min: Date): ValidatorFn {
return (c: AbstractControl) => {
if (c == null || c.value == null)
return null;
let isValid = c.value >= min;
if (isValid) {
return null;
} else {
return {
validateMinDate: {
valid: false
}
};
}
};
}
I initate my form like this
this.definitionForm = this.fb.group({
"from": [this.details.From, Validators.required],
"to": [this.details.To, [Validators.required, validateMinDate(this.details.From)]]
});
I can see that the validator is being applied, but when I console.log() my min value in the validator I can see that it equal null.
this.details.From starts at null when I initiate the form, so I assume the parameter is not dynamic and just takes the value when the form is being set?
How can I make sure the min date is being updated when a users picks a from date, and thus changes the value of this.details.From?
#Nicolas Validator takes value only once it does not look for it changes. So we can change parameters value dynamically by assigning new validator on value changes. In your case you can do in this way:
onChanges(){
var self=this;
this.definitionForm.get('from').valueChanges.subscribe(val => {
this.from=val;
this.definitionForm.controls['to'].
setValidators(Validators.compose([Validators.required,
TimeValidators.isTimeAfter(this.from)]));
})}
Here i created a separate custom validator for comparing the time. You can either use this or modify yours
import { FormControl, Validators,ValidatorFn, AbstractControl} from '#angular/forms';
export class TimeValidators extends Validators{
static isTimeBefore(timeStr : string): ValidatorFn{
return(c: AbstractControl): {[key:string]: boolean} | null => {
if(c.value!==undefined && (isNaN(c.value)) || c.value > timeStr || c.value== timeStr){
return {
'isTimeBefore':true
}
}
return null;
}
}
static isTimeAfter(timeStr : string): ValidatorFn{
return(c: AbstractControl): {[key:string]: boolean} | null => {
if(c.value!==undefined && (isNaN(c.value)) && (c.value < timeStr || c.value == timeStr)){
return {
'isTimeAfter':true
}
}
return null;
}
}
}
Call onChanges() function after you initialize your definitionForm FormGroup.
You can modify your custom validator to take function as parameter like
export function validateMinDate(min: DateFunc): ValidatorFn {
return (c: AbstractControl) => {
if (c == null || c.value == null)
return null;
let isValid = c.value >= min();
if (isValid) {
return null;
} else {
return {
validateMinDate: {
valid: false
}
};
}
};
and initiate the form like this
this.definitionForm = this.fb.group({
...
"to": [this.details.To, [Validators.required, validateMinDate(() => this.details.From)]]
});
the DateFunc is just a type that you can create like
export interface DateFunc{
(): Date
}
and expect this.details.From to return value of type Date
How I see it, would be to apply the validator on the form group, or if you have a large form, I suggest you create a nested group for from and to and apply the validator on that, since otherwise this custom validator would be fired whenever any changes happen to form. So it would mean to update the validator and formgroup to such:
this.definitionForm = this.fb.group({
"from": [this.details.From, Validators.required],
"to": [this.details.To, [Validators.required]]
}, {validator: validateMinDate()});
export function validateMinDate(): ValidatorFn {
return (c: AbstractControl) => {
if(c) {
let isValid = c.get('to').value >= c.get('from').value;
if (isValid) {
return null;
} else {
return {validateMinDate: true};
}
}
};
}
Of course there are other options as well, such as listening for change event and then do the check of the dates, if not valid, use setErrors on form.
As an alternative to the given answers, at least in Angular 6 you can pass the component as ValidatorFn argument, so you can use its properties at runtime to validate your form control.
Here is a working example:
Component declarations:
#Component({
templateUrl: './create-application.component.html'
})
export class CreateApplicationComponent implements OnInit, OnDestroy {
viewMode: ViewMode;
application: Application;
form: FormGroup;
enterprisesData: any[] = [];
Form:
this.form = this.formBuilder.group({
name: new FormControl(
{
value: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT) ? this.application.name : '',
disabled: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT)
},
{
validators: [ Validators.required, Validators.minLength(3), Validators.maxLength(80) ],
updateOn: 'blur'
}
),
area: new FormControl(
{
value: (this.viewMode === ViewMode.CONSULT || this.viewMode === ViewMode.EDIT) ? this.application.area : '',
disabled: (this.viewMode === ViewMode.CONSULT)
},
{
validators: [ Validators.required ]
}
),
country: new FormControl(
{
value: '',
disabled: (this.viewMode === ViewMode.CONSULT)
},
{
validators: [ applicationCountryEnterprisesValidator(this) ]
}
)
});
ValidatorFn:
export function applicationCountryEnterprisesValidator(component: CreateApplicationComponent): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
return (component.enterprisesData && component.enterprisesData.length > 0) ? null : { noEnterprisesSelected: true };
};
}

Categories