Jay Culpepper
Jay Culpepper

Reputation: 567

Kendo-angular-dropdown upgrade causing error

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

Answers (1)

Jay Culpepper
Jay Culpepper

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

Related Questions