Reputation: 567
I recently upgrade my work application from Angular 6 to Angular 7.2.
This included upgrading Kendo-Angular components/modules.
After upgrade, and during regression testing it was noticed that on a kendo-combobox, that uses a template, we are getting the following error:
V2: CustomExceptionHandler Error occurred TypeError: Cannot read property 'scrollToItem' of undefined
at main.js:89941
at SafeSubscriber.schedulerFn [as _next] (main.js:14671)
at SafeSubscriber.__tryOrUnsub (main.js:45229)
at SafeSubscriber.next (main.js:45167)
at Subscriber._next (main.js:45113)
at Subscriber.next (main.js:45090)
at EventEmitter.Subject.next (main.js:86078)
at EventEmitter.emit (main.js:14643)
at PopupComponent.onAnimationEnd (main.js:125483)
at main.js:125443
This looks like it is coming from kendo-popup, but I cant seem to fix it.
What I have tried:
upgraded to the most recent version of kendo-angular-dropdowns and kendo-angular-popup
Delete node-modules folder and npm cache clear
Downgrading back to [email protected] fixes it, but if I upgrade to 3.5.0 or above the error returns. I am at a loss on how to get kendo-angular-dropdowns to the most recent version and resolve this error.
Thanks
Dependencies from package.json
"dependencies": {
"@angular/animations": "7.2.16",
"@angular/cdk": "7",
"@angular/common": "7.2.16",
"@angular/compiler": "7.2.16",
"@angular/compiler-cli": "7.2.16",
"@angular/core": "7.2.16",
"@angular/forms": "7.2.16",
"@angular/http": "7.2.16",
"@angular/material": "7",
"@angular/platform-browser": "7.2.16",
"@angular/platform-browser-dynamic": "7.2.16",
"@angular/platform-server": "7.2.16",
"@angular/router": "7.2.16",
"@ngrx/effects": "^7.4.0",
"@ngrx/store": "^7.4.0",
"@ngrx/store-devtools": "^7.4.0",
"@ngui/auto-complete": "0.16.0",
"@progress/kendo-angular-buttons": "5.4.2",
"@progress/kendo-angular-charts": "4.1.4",
"@progress/kendo-angular-common": "1.2.3",
"@progress/kendo-angular-dateinputs": "4.2.2",
"@progress/kendo-angular-dialog": "4.2.0",
"@progress/kendo-angular-dropdowns": "3.4.0",
"@progress/kendo-angular-excel-export": "3.1.3",
"@progress/kendo-angular-grid": "4.7.2",
"@progress/kendo-angular-inputs": "6.6.0",
"@progress/kendo-angular-intl": "2.0.1",
"@progress/kendo-angular-l10n": "2.0.1",
"@progress/kendo-angular-layout": "4.2.1",
"@progress/kendo-angular-pdf-export": "2.0.3",
"@progress/kendo-angular-popup": "3.0.6",
"@progress/kendo-angular-ripple": "2.0.1",
"@progress/kendo-angular-scrollview": "3.0.1",
"@progress/kendo-angular-sortable": "3.0.2",
"@progress/kendo-angular-tooltip": "2.1.3",
"@progress/kendo-angular-treeview": "4.2.0",
"@progress/kendo-angular-upload": "6.0.0",
"@progress/kendo-data-query": "1.5.4",
"@progress/kendo-drawing": "1.9.2",
"@progress/kendo-theme-default": "2.54.0",
"@progress/kendo-theme-material": "^1.5.0",
"@types/clipboard": "1.5.35",
"@types/file-saver": "1.3.0",
"@types/highcharts": "5.0.11",
"@types/lodash": "^4.14.62",
"@types/moment-timezone": "^0.5.4",
"angular-l10n": "7.2.0",
"angular-resizable-element": "^3.3.3",
"angular-router-loader": "0.7.0",
"angular2-busy": "^2.0.4",
"aspnet-prerendering": "3.0.1",
"aspnet-webpack": "1.0.29",
"babel-polyfill": "^6.23.0",
"bootstrap": "3.3.7",
"clipboard": "^1.6.1",
"compression-webpack-plugin": "1.0.1",
"exceljs": "3.5.0",
"file-saver": "1.3.3",
"font-awesome": "4.7.0",
"hammerjs": "^2.0.8",
"highcharts": "6.0.3",
"html-webpack-plugin": "^2.28.0",
"husky": "^0.14.3",
"isomorphic-fetch": "2.2.1",
"lodash": "^4.17.4",
"lscache": "1.1.0",
"moment": "^2.17.1",
"moment-timezone": "^0.5.14",
"ng2-appinsights": "^1.0.0-beta.1",
"ng2-auto-complete": "^0.12.0",
"ng2-dnd": "^5.0.2",
"ngx-bootstrap": "2.0.0-beta.1",
"ngx-tinymce": "^7.0.0",
"ngx-toastr": "8.8.0",
"normalize.css": "7.0.0",
"optimize-js-plugin": "^0.0.4",
"preboot": "5.1.7",
"rxjs": "6.6.3",
"rxjs-compat": "^6.6.3",
"zone.js": "0.8.29"
},
Component HTML
<div *ngIf="ttsConfig?.formgroup" #form="ngForm" [formGroup]="ttsConfig.formgroup">
<!--value:{{ttsConfig.formgroup.controls.counterpartyId.value}}-->
<div class="tts-wrapper" >
<span class="tts-fieldname">{{ttsConfig.translate? friendlyFieldName : ttsConfig.friendlyFieldName}}<span *ngIf="ttsConfig.required" style="padding-left:3px;">*</span></span>
<div *ngIf="!AuthInfo">
<kendo-combobox #autocomplete [id]="id"
[data]="data"
[popupSettings]="{width: ttsConfig.width, appendTo: ViewContainerRef, animate: false}"
[filterable]="true"
[formControlName]="ttsConfig.formcontrolname"
(filterChange)="callDebouncer($event)"
[valueField]="ttsConfig.apiIdName"
[textField]="ttsConfig.displayTextColoumn"
[valuePrimitive]="true"
(open)="checkMinLength($event)"
[placeholder]="ttsConfig.translate? placeholder : ttsConfig.placeholder"
[required]="ttsConfig.required"
class="{{ttsConfig.cssClass}}"
(valueChange)="onGridSelectionChange($event)">
<ng-template kendoAutoCompleteHeaderTemplate>
<table width="{{ttsConfig.width}}">
<tr>
<td *ngFor="let i of ttsConfig.columns" style="font-weight:bold"><span *ngIf="ttsConfig.translate" l10nTranslate>{{i}}</span><span *ngIf="!ttsConfig.translate">{{i}}</span></td>
</tr>
</table>
</ng-template>
<ng-template kendoAutoCompleteItemTemplate let-dataItem>
<table width="{{ttsConfig.width}}">
<tr>
<td *ngFor="let i of ttsConfig.apiDisplayObjectName">
{{ dataItem[i] }}
</td>
</tr>
</table>
</ng-template>
</kendo-combobox>
<button *ngIf="ttsConfig.displayIcon === true" #anchor (click)="toggle()" class="k-button k-button-icon" [hidden]="data.length === 0"><i class="fa fa-info" aria-hidden="true"></i></button>
<div class="custom-error-tts" *ngIf="ttsConfig.formgroup.controls[ttsConfig.formcontrolname]?.errors && ttsConfig.formgroup.controls[ttsConfig.formcontrolname].touched === true">
<strong l10nTranslate>{{ttsConfig.formgroup.controls[ttsConfig.formcontrolname].errors | errorPipe}}</strong>
</div>
</div>
<div *ngIf="AuthInfo">
<kendo-combobox #autocomplete [id]="id"
[data]="data"
[popupSettings]="{width: ttsConfig.width, appendTo: ViewContainerRef, animate: false}"
[filterable]="true"
[formControlName]="ttsConfig.formcontrolname"
(filterChange)="callDebouncer($event)"
[valueField]="ttsConfig.apiIdName"
[textField]="ttsConfig.displayTextColoumn"
[valuePrimitive]="true"
(open)="checkMinLength($event)"
[placeholder]="ttsConfig.translate? placeholder : ttsConfig.placeholder"
[required]="ttsConfig.required"
class="{{ttsConfig.cssClass}}"
(valueChange)="onGridSelectionChange($event)" counterpartyAccess [authInfo]="AuthInfo">
<!-- <ng-template kendoAutoCompleteHeaderTemplate>
<table width="{{ttsConfig.width}}">
<tr>
<td *ngFor="let i of ttsConfig.columns" style="font-weight:bold"><span *ngIf="ttsConfig.translate" l10nTranslate>{{i}}</span><span *ngIf="!ttsConfig.translate">{{i}}</span></td>
</tr>
</table>
</ng-template>
<ng-template kendoAutoCompleteItemTemplate let-dataItem>
<table width="{{ttsConfig.width}}">
<tr>
<td *ngFor="let i of ttsConfig.apiDisplayObjectName">
{{ dataItem[i] }}
</td>
</tr>
</table>
</ng-template> -->
</kendo-combobox>
<button *ngIf="ttsConfig.displayIcon === true" #anchor (click)="toggle()" class="k-button k-button-icon" [hidden]="data.length === 0"><i class="fa fa-info" aria-hidden="true"></i></button>
<div class="custom-error-tts" *ngIf="ttsConfig.formgroup.controls[ttsConfig.formcontrolname]?.errors && ttsConfig.formgroup.controls[ttsConfig.formcontrolname].touched === true">
<strong l10nTranslate>{{ttsConfig.formgroup.controls[ttsConfig.formcontrolname].errors | errorPipe}}</strong>
</div>
</div>
<kendo-popup #popup [anchor]="anchor" popupClass="content" *ngIf="show">
<table>
<tr>
<td *ngFor="let i of ttsConfig.columns" style="font-weight:bold"><span *ngIf="ttsConfig.translate" l10nTranslate>{{i}}</span><span *ngIf="!ttsConfig.translate">{{i}}</span></td>
</tr>
</table>
<table *ngIf="data.length > 0">
<tr>
<td style="max-height: 100px; max-width:200px; white-space:pre-wrap;" *ngFor="let i of ttsConfig.apiDisplayObjectName">
{{ data[0][i] }}
</td>
</tr>
</table>
</kendo-popup>
</div>
<span *ngIf="IsMDM">*MDM</span>
</div>
Component.ts file
import { Component, ViewChild, Input, Output, EventEmitter, ElementRef, HostListener, OnInit, OnChanges, OnDestroy, LOCALE_ID, Inject } from '@angular/core';
import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms';
import { AutoCompleteComponent } from '@progress/kendo-angular-dropdowns';
import { DropdownService } from '@ldc/core/shared/services';
import { takeUntil, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { Subject } from 'rxjs';
//import { TranslationService } from 'angular-l10n';
import { LdcLocale, LocaleService, TranslationService } from '@ldc/core/shared/classes/LdcLocale';
import { LocaleSubscriptionService } from '@ldc/core/shared/services';
import { CldrIntlService } from '@progress/kendo-angular-intl';
@Component({
selector: 'ldc-type-to-search',
templateUrl: './type-to-search.component.html',
styleUrls: ['./type-to-search.component.scss']
})
export class TypeToSearchComponent extends LdcLocale implements OnInit, OnChanges, OnDestroy {
@ViewChild('autocomplete') public autocomplete: AutoCompleteComponent;
@Input() ttsConfig: TypeToSearchConfig = new TypeToSearchConfig();
@Input() id: string = undefined;
@Output() public selectedItemChange: EventEmitter<any> = new EventEmitter();
@ViewChild('anchor') public anchor: ElementRef;
@ViewChild('popup', { read: ElementRef }) public popup: ElementRef;
@Input() IsMDM = false;
@Input() AuthInfo: any;
public gridSelection: string[] = [];
public dataList: any[] = [];
public data: any[] = [];
private formSubscribed: boolean = false;
private ngUnsubscribe = new Subject();
private debouncer: Subject<any> = new Subject();
public value: any;
private toggleText = 'Show';
private show = false;
private initalLoadComplete = false;
private friendlyFieldName: string = '';
private placeholder: string = '';
constructor(@Inject(LOCALE_ID) public localeId: string,
private dropdownService: DropdownService,
private localeService: LocaleService,
private localeSubscriptionService: LocaleSubscriptionService,
private intlService: CldrIntlService,
private translationService: TranslationService) {
super(localeService, translationService);
}
ngOnInit() {
this.localeSubscriptionService.currentLocaleId.pipe(takeUntil(this.ngUnsubscribe)).subscribe(localeId => this.setLocaleId(localeId));
if (this.ttsConfig.apiByTermTypeNoSearchUrl) {
this.callApi();
}
this.debouncer.pipe(takeUntil(this.ngUnsubscribe), debounceTime(100)).subscribe(event => {
this.handleFilter(event);
});
}
ngOnChanges(change) {
if (change.ttsConfig && change.ttsConfig.currentValue.formgroup !== null && this.formSubscribed === false) {
this.formSubscribed = true;
if (this.ttsConfig.formgroup.value[this.ttsConfig.formcontrolname] !== null && change.previousValue === undefined) {
this.callApi(this.ttsConfig.formgroup.value[this.ttsConfig.formcontrolname]);
}
//this was originally subscribed to the entire form and could negatively impact performance
//we need to only subscribe to the control itself
this.ttsConfig.formgroup.controls[this.ttsConfig.formcontrolname].valueChanges.pipe(takeUntil(this.ngUnsubscribe)).subscribe((val) => {
if (val !== null) {
this.callApi(val);
}
});
let formcontrolname = this.ttsConfig.formcontrolname;
let formgroup = this.ttsConfig.formgroup;
let formControlValue = formgroup.controls[formcontrolname].value;
if (this.ttsConfig.useDirectBinding === true && formControlValue !== undefined && formControlValue !== null && formControlValue !== '') {
this.callApi(formControlValue);
}
}
}
setLocaleId(localeId: string) {
this.localeId = localeId;
this.intlService.localeId = localeId;
if (this.ttsConfig.translate) {
this.translationService.init().then((data) => {
this.placeholder = this.translationService.translate(this.ttsConfig.placeholder, null, this.localeService.getCurrentLanguage());
this.friendlyFieldName = this.translationService.translate(this.ttsConfig.friendlyFieldName, null, this.localeService.getCurrentLanguage());
});
}
}
ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
private callDebouncer(event) {
this.debouncer.next(event);
}
public onGridSelectionChange(val): void {
if (!val) {
this.selectedItemChange.emit({ value: null, control: this.ttsConfig.formcontrolname });
return;
}
if (this.ttsConfig.emitPrimitive) {
if (this.ttsConfig.apiUrl === 'CRS/ViewTTSGinInvoiceCounterparty/') {
let selectedobj = this.dataList.find(x => x[this.ttsConfig.apiIdName] == val);
this.selectedItemChange.emit({ value: selectedobj, control: this.ttsConfig.formcontrolname });
} else {
this.selectedItemChange.emit({ value: val, control: this.ttsConfig.formcontrolname });
}
} else {
let selectedobj = this.dataList.find(x => x[this.ttsConfig.apiIdName] == val);
this.selectedItemChange.emit({ value: selectedobj, control: this.ttsConfig.formcontrolname });
}
this.value = val;
}
handleFilter(value) {
if (value.length >= this.ttsConfig.minLength) {
this.callApi(value);
} else {
this.autocomplete.toggle(false);
}
}
checkMinLength(e) {
if (this.autocomplete.text.length >= this.ttsConfig.minLength || this.ttsConfig.apiByTermTypeNoSearchUrl) {
} else {
this.autocomplete.toggle(false);
}
}
callApi(searchTerm?) {
if (searchTerm !== undefined && searchTerm.trim().length > 0) {
let term = `${this.ttsConfig.apiUrl}${encodeURIComponent(searchTerm)}`;
this.dropdownService.getData(term, 0).pipe(takeUntil(this.ngUnsubscribe)).subscribe(resp => {
this.dataList = resp;
this.data = this.dataList.slice();
const fgvalue = this.ttsConfig.formgroup.controls[this.ttsConfig.formcontrolname].value;
if (this.ttsConfig.emitInitalLoad && !this.initalLoadComplete && fgvalue) {
this.onGridSelectionChange(fgvalue);
this.initalLoadComplete = true;
}
if (this.ttsConfig.apiByTermTypeNoSearchUrl) {
this.data.sort((a, b) => a.TermDescription.localeCompare(b.TermDescription));
}
});
} else {
let term = this.ttsConfig.apiByTermTypeNoSearchUrl ? `${this.ttsConfig.apiByTermTypeNoSearchUrl}` : undefined;
if (term) {
this.dropdownService.getData(term, 0).pipe(takeUntil(this.ngUnsubscribe)).subscribe(resp => {
this.dataList = resp;
this.data = this.dataList.slice();
this.data.sort((a, b) => a.TermDescription.localeCompare(b.TermDescription));
});
}
}
}
@HostListener('keydown', ['$event'])
public keydown(event: any): void {
if (event.keyCode === 27) { //escape key
this.toggle(false);
}
}
@HostListener('document:click', ['$event'])
public documentClick(event: any): void {
if (this.ttsConfig.displayIcon && this.data.length > 0) {
if (!this.contains(event.target)) {
this.toggle(false);
}
}
}
public toggle(show?: boolean): void {
this.show = show !== undefined ? show : !this.show;
this.toggleText = this.show ? 'Hide' : 'Show';
}
private contains(target: any): boolean {
if (this.ttsConfig.displayIcon && this.data.length > 0) {
return this.anchor.nativeElement.contains(target) ||
(this.popup ? this.popup.nativeElement.contains(target) : false);
} else {
return false;
}
}
}
export class TypeToSearchConfig {
columns: string[];
required: boolean;
apiUrl: string;
apiDisplayObjectName: string[];
displayTextColoumn: string;
apiIdName: string;
formcontrolname: string;
formgroup: FormGroup;
placeholder: string;
friendlyFieldName: string;
width: number;
cssClass: string;
displayIcon: boolean;
minLength: number;
apiByTermTypeNoSearchUrl?: string;
emitPrimitive: boolean;
emitInitalLoad: boolean;
useDirectBinding: boolean;
translate: boolean;
constructor(
columns: string[] = [],
requried: boolean = false,
apiUrl: string = null,
apiDisplayObjectName: string[] = null,
displayTextColoumn: string = null,
apiIdName: string = null,
fromcontrolname: string = null,
formgroup: FormGroup = null,
placeholder: string = '',
friendlyFieldName: string = '',
width: number = 750,
cssClass: string = 'lc-combobox',
displayIcon: boolean = true,
minLength: number = 3,
apiByTermTypeNoSearchUrl: string = null,
translate: boolean = false) {
this.columns = columns;
this.required = requried;
this.apiUrl = apiUrl;
this.displayTextColoumn = displayTextColoumn;
this.apiDisplayObjectName = apiDisplayObjectName;
this.apiIdName = apiIdName;
this.formcontrolname = fromcontrolname;
this.formgroup = formgroup;
this.placeholder = placeholder;
this.friendlyFieldName = friendlyFieldName;
this.width = width;
this.cssClass = cssClass;
this.displayIcon = displayIcon;
this.minLength = minLength;
this.apiByTermTypeNoSearchUrl = apiByTermTypeNoSearchUrl;
this.translate = translate;
this.emitPrimitive = true;
this.emitInitalLoad = false;
this.useDirectBinding = false;
}
}
``
Upvotes: 0
Views: 942
Reputation: 567
so the issue I was having was that I did not want to show the popup in the combo box until the usr has entered a minimum # of characters in the input.
Previously (another dev) had done this by hooking into the combobox's open event (open)="checkMinLength($event)"
which did this:
checkMinLength(e) {
if (this.autocomplete.text.length <= this.minLength) {
this.autocomplete.toggle(false);
}
}
I found a telerk support question here that deals with a similar problem.
basically the solution for me was to replace this.autocomplete.toggle(false);
with e.preventDefault();
Upvotes: 0