@progress/kendo-angular-listview
Version:
Kendo UI Angular listview component
1,301 lines (1,283 loc) • 84.3 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import * as i0 from '@angular/core';
import { Injectable, Directive, Input, Optional, EventEmitter, Component, ChangeDetectionStrategy, HostBinding, ContentChild, ViewChild, ViewChildren, Output, HostListener, NgModule } from '@angular/core';
import { isDocumentAvailable, normalizeKeys, Keys, isChanged, hasObservers, EventsOutsideAngularDirective, ResizeBatchService } from '@progress/kendo-angular-common';
import { validatePackage } from '@progress/kendo-licensing';
import * as i2 from '@progress/kendo-angular-l10n';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { Subject, Subscription } from 'rxjs';
import { FormGroup, FormControl } from '@angular/forms';
import { switchMap, take } from 'rxjs/operators';
import { NgTemplateOutlet, NgClass, NgStyle } from '@angular/common';
import { PagerComponent, PageSizeChangeEvent as PageSizeChangeEvent$1 } from '@progress/kendo-angular-pager';
import { Button } from '@progress/kendo-angular-buttons';
import { IconWrapperComponent, IconsService } from '@progress/kendo-angular-icons';
import { PopupService } from '@progress/kendo-angular-popup';
/**
* @hidden
*/
const packageMetadata = {
name: '@progress/kendo-angular-listview',
productName: 'Kendo UI for Angular',
productCode: 'KENDOUIANGULAR',
productCodes: ['KENDOUIANGULAR'],
publishDate: 1765468203,
version: '21.3.0',
licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/'
};
const LISTVIEW_ITEM_SELECTOR = '.k-listview-item';
/**
* @hidden
*/
const isPresent = (item) => item !== null && item !== undefined;
/**
* @hidden
*/
const isObject = (item) => isPresent(item) && typeof item === 'object';
/**
* @hidden
*
* Polyfill for `Element.matches`.
* https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
*/
const match = (element, selector) => {
const matcher = element.matches || element.msMatchesSelector || element.webkitMatchesSelector;
if (!isPresent(matcher)) {
return false;
}
return matcher.call(element, selector);
};
/**
* @hidden
*
* Checks if a target element has the `.k-listview-item` CSS class.
*/
const isListViewItem = (element) => {
if (!isPresent(element)) {
return false;
}
return match(element, LISTVIEW_ITEM_SELECTOR);
};
/**
* @hidden
*
* Extracts and parses to a number the `data-kendo-listview-item-index` attribute value from the targeted element.
*/
const getListItemIndex = (item) => {
if (!isPresent(item)) {
return null;
}
return Number(item.getAttribute('data-kendo-listview-item-index'));
};
/**
* @hidden
*
* Gets the new focus target from a blur event.
* Queries both event.relatedTarget and document.activeElement for compatibility with IE.
*/
const relatedTarget = (event) => {
if (!isPresent(event.relatedTarget) || !isDocumentAvailable()) {
return null;
}
return event.relatedTarget || document.activeElement;
};
/**
* @hidden
*
* If the given contender number is not defined or lower than the specified min - returns min, if its above the specified max - returns max.
* If the number is in the given bounds, it is returned.
*/
const fitIntoRange = (contender, min, max) => {
if (!isPresent(contender) || contender <= min) {
return min;
}
else if (contender >= max) {
return max;
}
else {
return contender;
}
};
/**
* @hidden
*/
const closestWithMatch = (element, selector) => {
let parent = element;
while (parent !== null && parent.nodeType === 1) {
if (match(parent, selector)) {
return parent;
}
parent = parent.parentElement || parent.parentNode;
}
return null;
};
/**
* @hidden
*
* Extracts and parses to a number the `data-kendo-listview-item-index` attribute value from the targeted element.
*/
const getClosestListItemIndex = (element) => {
if (!isPresent(element)) {
return null;
}
const closestListViewItem = closestWithMatch(element, LISTVIEW_ITEM_SELECTOR);
return getListItemIndex(closestListViewItem);
};
/**
* @hidden
*
* Provided per ListView instance. Keeps the availability, active index and focused state of the current ListView.
* Emits `changes` when any of the aforementioned states changes.
*/
class NavigationService {
/**
* Emits every time a change in active index/focus/blur/navigation availability occurs.
*/
changes = new Subject();
/**
* Sets or gets if the navigation is enabled.
* When no activeIndex is present, the navigation is inferred as disabled.
* Toggling the service availability clears the current active index or activates the first one.
*/
get isEnabled() {
return isPresent(this.activeIndex);
}
set isEnabled(enabled) {
if (enabled) {
this.activeIndex = 0;
}
else {
this.activeIndex = null;
}
this.changes.next(undefined);
}
/**
* Specifies if a ListView item currently holds focus.
*/
isFocused = false;
/**
* Keeps track of the index of the items that should be the current focus target (tabindex="0").
* When set to `null`/`undefined`, the navigation is disabled and the items should not render a tabindex.
*/
activeIndex = null;
/**
* Shows if the checked index should be the current available focus target (tabindex="0").
*/
isActive(index) {
return index === this.activeIndex;
}
handleKeyDown(event, totalItemsCount) {
// on some keyboards arrow keys, PageUp/Down, and Home/End are mapped to Numpad keys
const code = normalizeKeys(event);
switch (code) {
case Keys.ArrowLeft:
case Keys.ArrowUp:
this.navigateToPrevious();
break;
case Keys.ArrowRight:
case Keys.ArrowDown:
this.navigateToNext(totalItemsCount);
break;
case Keys.Home: {
const firstIndex = 0;
this.navigateTo(firstIndex);
break;
}
case Keys.End: {
const lastIndex = totalItemsCount - 1;
this.navigateTo(lastIndex);
break;
}
default: return;
}
// the following line is executed only if the pressed key matches one of the listview hotkeys -
// they `break`, while the `default` case `return`s
event.preventDefault();
}
handleFocusIn(event) {
const target = event.target;
if (!isListViewItem(target)) {
const listViewItemSelector = '.k-listview-item';
const closestListViewItem = closestWithMatch(target, listViewItemSelector);
const isListViewItemChild = isPresent(closestListViewItem);
if (isListViewItemChild) {
const itemIndex = getListItemIndex(closestListViewItem);
this.setActiveIndex(itemIndex);
}
return;
}
const targetIndex = getListItemIndex(target);
// don't emit if the no change in focused state has occurred and the targeted index is the same as the current activeIndex
if (this.isFocused && targetIndex === this.activeIndex) {
return;
}
this.activeIndex = targetIndex;
this.isFocused = true;
this.changes.next(undefined);
}
handleFocusOut(event) {
// don't emit if the blurred item is not a listview item or if the new focus target is another listview item
if (!isListViewItem(event.target) || isListViewItem(relatedTarget(event))) {
return;
}
this.isFocused = false;
this.changes.next(undefined);
}
/**
* Sets the `activeIndex` and triggers changes without focusing the corresponding ListView item.
*/
setActiveIndex(index) {
if (!this.isEnabled) {
return;
}
if (index === this.activeIndex) {
return;
}
this.activeIndex = index;
this.changes.next(undefined);
}
/**
* Focuses item at the targeted index. If no index is passed, the current `activeIndex` is used.
* The passed target index is normalized to fit the min/max available indices bounds.
*/
focusIndex(index, totalItemsCount) {
if (!this.isEnabled) {
return;
}
const parsed = parseInt(index, 10);
const firstIndex = 0;
const lastIndex = totalItemsCount - 1;
const targetIndex = isNaN(parsed) ? this.activeIndex : fitIntoRange(parsed, firstIndex, lastIndex);
this.navigateTo(targetIndex);
}
navigateTo(index) {
if (this.isFocused && this.activeIndex === index) {
return;
}
this.isFocused = true;
this.activeIndex = index;
this.changes.next(undefined);
}
navigateToPrevious() {
const previousIndex = Math.max(this.activeIndex - 1, 0);
this.navigateTo(previousIndex);
}
navigateToNext(totalItemsCount) {
const lastAvailableIndex = totalItemsCount - 1;
const nextIndex = Math.min(this.activeIndex + 1, lastAvailableIndex);
this.navigateTo(nextIndex);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService, decorators: [{
type: Injectable
}] });
/**
* @hidden
*/
class ListViewNavigableItemDirective {
hostElement;
renderer;
navigationService;
/**
* The current item index. Used to track which navigation changes apply to this item.
*/
index;
navigationSubscription;
constructor(hostElement, renderer, navigationService) {
this.hostElement = hostElement;
this.renderer = renderer;
this.navigationService = navigationService;
}
ngOnChanges() {
this.updateNavigationState();
}
ngOnInit() {
this.navigationSubscription = this.navigationService.changes
.subscribe(this.updateNavigationState.bind(this));
}
ngOnDestroy() {
if (isPresent(this.navigationSubscription)) {
this.navigationSubscription.unsubscribe();
}
}
updateNavigationState() {
this.updateTabIndex();
this.updateFocusedState();
}
updateFocusedState() {
const shouldFocus = this.navigationService.isActive(this.index) && this.navigationService.isFocused;
const focusedCssClass = 'k-focus';
if (shouldFocus) {
this.renderer.addClass(this.hostElement.nativeElement, focusedCssClass);
this.hostElement.nativeElement.focus();
}
else {
this.renderer.removeClass(this.hostElement.nativeElement, focusedCssClass);
}
}
updateTabIndex() {
if (!this.navigationService.isEnabled) {
this.renderer.removeAttribute(this.hostElement.nativeElement, 'tabindex');
}
else if (this.navigationService.isActive(this.index)) {
this.renderer.setAttribute(this.hostElement.nativeElement, 'tabindex', '0');
}
else {
this.renderer.setAttribute(this.hostElement.nativeElement, 'tabindex', '-1');
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListViewNavigableItemDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: NavigationService }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: ListViewNavigableItemDirective, isStandalone: true, selector: "[kendoListViewNavigableItem]", inputs: { index: "index" }, usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListViewNavigableItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoListViewNavigableItem]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: NavigationService }], propDecorators: { index: [{
type: Input
}] } });
/**
* Allows customizing the list item content. To define an item template, nest an `<ng-template>` tag
* with the `kendoListViewItemTemplate` directive inside the `<kendo-listview>` tag
* ([see example]({% slug templates_listview %}#toc-item-template)).
*
* The following values are available as context variables:
* - `let-dataItem="dataItem"` (`any`)&mdashThe current data item. Also available as implicit context variable.
* - `let-index="index"` (`number`)&mdashThe current item index.
* - `let-isFirst="isFirst"` (`boolean`)&mdashIndicates whether the current data item renders as the first item on the list.
* - `let-isLast="isLast"` (`boolean`)&mdashIndicates whether the current data item renders as the last item on the list.
*
* @example
* ```typescript
* @Component({
* template: `
* <kendo-listview [data]="items">
* <ng-template kendoListViewItemTemplate let-dataItem let-index="index">
* <div class="item-wrapper">
* <h4>{{ dataItem.name }}</h4>
* <p>Item #{{ index + 1 }}: {{ dataItem.description }}</p>
* </div>
* </ng-template>
* </kendo-listview>
* `
* })
* export class AppComponent { }
* ```
*/
class ItemTemplateDirective {
templateRef;
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ItemTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: ItemTemplateDirective, isStandalone: true, selector: "[kendoListViewItemTemplate]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ItemTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoListViewItemTemplate]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
/**
* Allows customizing the header content of the ListView. To define a header template, nest an `<ng-template>` tag
* with the `kendoListViewHeaderTemplate` directive inside the `<kendo-listview>` tag
* ([see example]({% slug templates_listview %}#toc-header-template)).
*
* @example
* ```typescript
* @Component({
* template: `
* <kendo-listview [data]="items">
* <ng-template kendoListViewHeaderTemplate>
* <div class="header-content">
* <h3>Product List</h3>
* <button kendoListViewAddCommand>Add New Item</button>
* </div>
* </ng-template>
* </kendo-listview>
* `
* })
* export class AppComponent { }
* ```
*/
class HeaderTemplateDirective {
templateRef;
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HeaderTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: HeaderTemplateDirective, isStandalone: true, selector: "[kendoListViewHeaderTemplate]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HeaderTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoListViewHeaderTemplate]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
/**
* Allows customizing the footer content of the ListView. To define a footer template, nest an `<ng-template>` tag
* with the `kendoListViewFooterTemplate` directive inside the `<kendo-listview>` tag
* ([see example]({% slug templates_listview %}#toc-footer-template)).
*
* @example
* ```typescript
* @Component({
* template: `
* <kendo-listview [data]="items">
* <ng-template kendoListViewFooterTemplate>
* <div class="footer-content">
* <p>Total items: {{ items.length }}</p>
* </div>
* </ng-template>
* </kendo-listview>
* `
* })
* export class AppComponent { }
* ```
*/
class FooterTemplateDirective {
templateRef;
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FooterTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: FooterTemplateDirective, isStandalone: true, selector: "[kendoListViewFooterTemplate]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FooterTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoListViewFooterTemplate]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
/**
* Overrides the default loader content of the ListView. To define a loader template, nest an `<ng-template>` tag
* with the `kendoListViewLoaderTemplate` directive inside the `<kendo-listview>` tag
* ([see example]({% slug templates_listview %}#toc-loader-template)).
*
* @example
* ```typescript
* @Component({
* template: `
* <kendo-listview [data]="items" [loading]="isLoading">
* <ng-template kendoListViewLoaderTemplate>
* <div class="custom-loader">
* <kendo-loader></kendo-loader>
* <p>Loading data, please wait...</p>
* </div>
* </ng-template>
* </kendo-listview>
* `
* })
* export class AppComponent { }
* ```
*/
class LoaderTemplateDirective {
templateRef;
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoaderTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: LoaderTemplateDirective, isStandalone: true, selector: "[kendoListViewLoaderTemplate]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoaderTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoListViewLoaderTemplate]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
/**
* Defines the edit template of the ListView ([see example]({% slug editing_template_forms_listview %})).
* Helps you customize the content of the edited items. To define the template, nest an `<ng-template>`
* tag with the `kendoListViewEditTemplate` directive inside a `<kendo-listview>` tag.
*
* The template context contains the following fields:
* - `formGroup`—The current [`FormGroup`](link:site.data.urls.angular['formgroupapi']). When you use the ListView inside [Template-Driven Forms](link:site.data.urls.angular['forms']), it will be `undefined`.
* - `itemIndex`—The current item index. When inside a new item, `itemIndex` is `-1`.
* - `dataItem`—The current data item.
* - `isNew`—The state of the current item.
*
*
* @example
* ```typescript
* @Component({
* template: `
* <kendo-listview [data]="items">
* <ng-template kendoListViewEditTemplate let-dataItem let-formGroup="formGroup">
* <div class="edit-form">
* <input [(ngModel)]="dataItem.name" [formControl]="formGroup.get('name')" />
* <button kendoListViewSaveCommand>Save</button>
* <button kendoListViewCancelCommand>Cancel</button>
* </div>
* </ng-template>
* </kendo-listview>
* `
* })
* export class AppComponent { }
* ```
*/
class EditTemplateDirective {
templateRef;
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EditTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: EditTemplateDirective, isStandalone: true, selector: "[kendoListViewEditTemplate]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EditTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoListViewEditTemplate]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.TemplateRef, decorators: [{
type: Optional
}] }] });
/**
* @hidden
*/
const isEqual = (index) => (item) => item.index === index;
/**
* @hidden
*/
const isNotEqual = (index) => (item) => item.index !== index;
/**
* @hidden
*/
const isNewItem = (index) => index === -1 || index === undefined;
/**
* @hidden
*/
class EditService {
ngZone;
changes = new EventEmitter();
changed;
editedIndices = [];
newItem;
changedSource = new Subject();
constructor(ngZone) {
this.ngZone = ngZone;
this.changed = this.changedSource.asObservable().pipe(switchMap(() => this.ngZone.onStable.asObservable().pipe(take(1))));
}
editItem(index, group = undefined) {
this.editedIndices.push({ index, group });
this.onChanged();
}
addItem(group) {
this.newItem = { group };
this.onChanged();
}
isEditing() {
return this.editedIndices.length > 0;
}
get hasNewItem() {
return isPresent(this.newItem);
}
get newDataItem() {
if (this.hasNewItem) {
return this.newItem.group.value;
}
return {};
}
get newItemGroup() {
if (this.hasNewItem) {
return this.newItem.group;
}
return new FormGroup({});
}
editGroup(index) {
return this.context(index).group;
}
close(index) {
if (isNewItem(index)) {
this.newItem = undefined;
return;
}
this.editedIndices = this.editedIndices.filter(isNotEqual(index));
this.onChanged();
}
context(index) {
if (isNewItem(index)) {
return this.newItem;
}
return this.findByIndex(index);
}
isEdited(index) {
if (isNewItem(index) && isPresent(this.newItem)) {
return true;
}
return isPresent(this.findByIndex(index));
}
hasEdited(index) {
return isPresent(this.context(index));
}
beginEdit(itemIndex) {
this.changes.emit({ action: 'edit', itemIndex });
}
beginAdd() {
this.changes.emit({ action: 'add' });
}
endEdit(itemIndex) {
const { group: formGroup } = this.context(itemIndex);
this.changes.emit({ action: 'cancel', itemIndex, formGroup, isNew: isNewItem(itemIndex) });
}
save(itemIndex) {
const { group: formGroup } = this.context(itemIndex);
this.changes.emit({ action: 'save', itemIndex, formGroup, isNew: isNewItem(itemIndex) });
}
remove(itemIndex) {
this.changes.emit({ action: 'remove', itemIndex });
}
findByIndex(index) {
return this.editedIndices.find(isEqual(index));
}
onChanged() {
this.ngZone.runOutsideAngular(() => {
this.changedSource.next(undefined);
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EditService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EditService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EditService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i0.NgZone }] });
const DEFAULT_PAGER_SETTINGS = {
position: 'bottom',
buttonCount: 5,
info: true,
previousNext: true,
type: 'numeric',
pageSizeValues: [5, 10, 20]
};
const createControl = (source) => (acc, key) => {
acc[key] = new FormControl(source[key]);
return acc;
};
/**
* Represents the Kendo UI ListView component for Angular.
* Displays a list of data items and supports paging, editing, and custom templates
* ([see overview]({% slug overview_listview %})).
*
* @example
* ```typescript
* @Component({
* selector: 'my-app',
* template: `
* <kendo-listview
* [data]="items"
* [pageable]="true"
* [pageSize]="5">
* <ng-template kendoListViewItemTemplate let-dataItem>
* <div class="item">
* <h3>{{ dataItem.name }}</h3>
* <p>{{ dataItem.description }}</p>
* </div>
* </ng-template>
* </kendo-listview>
* `
* })
* export class AppComponent {
* items = [
* { name: 'Item 1', description: 'First item' },
* { name: 'Item 2', description: 'Second item' }
* ];
* }
* ```
*/
class ListViewComponent {
ngZone;
element;
renderer;
changeDetectorRef;
editService;
navigationService;
/**
* @hidden
*/
className = true;
/**
* @hidden
*/
itemTemplate;
/**
* @hidden
*/
headerTemplate;
/**
* @hidden
*/
footerTemplate;
/**
* @hidden
*/
loaderTemplate;
/**
* @hidden
*/
contentContainer;
/**
* @hidden
*/
editTemplate;
/**
* @hidden
*/
listViewItems;
/**
* Specifies if a border should be rendered around the listview element.
*
* @default false
*/
bordered = false;
/**
* Specifies the data collection that populates the ListView
* ([see data binding examples]({% slug paging_listview %})).
*/
set data(value) {
this.lastScrollTop = this.contentContainer?.nativeElement.scrollTop ?? 0;
this._data = value;
}
get data() {
return this._data;
}
/**
* Specifies whether the loading indicator of the ListView displays
* ([see example]({% slug paging_listview %}#toc-remote-binding)).
*
* @default false
*/
loading = false;
/**
* Specifies the CSS styles that render on the content container element of the ListView.
* Supports the type of values that [`ngStyle`](link:site.data.urls.angular['ngstyleapi']) supports.
*/
containerStyle;
/**
* Specifies the CSS styles that render on each item element wrapper of the ListView.
* Supports the type of values that [`ngStyle`](link:site.data.urls.angular['ngstyleapi']) supports.
*/
itemStyle;
/**
* Specifies the CSS class that renders on the content container element of the ListView.
* Supports the type of values that [`ngClass`](link:site.data.urls.angular['ngclassapi']) supports.
*/
containerClass;
/**
* Specifies the CSS class that renders on each item element wrapper of the ListView.
* Supports the type of values that [`ngClass`](link:site.data.urls.angular['ngclassapi']) supports.
*/
itemClass;
/**
* Specifies the content container `aria-label` attribute
* ([see example]({% slug accessibility_listview %}#toc-accessible-names)).
*/
containerLabel;
/**
* Specifies the content container `role` attribute
* ([more details]({% slug accessibility_listview %}#toc-wai-aria-support)).
*
* @default 'list'
*/
containerRole = 'list';
/**
* Specifies the list item `role` attribute
* ([more details]({% slug accessibility_listview %}#toc-wai-aria-support)).
*
* @default 'listitem'
*/
listItemRole = 'listitem';
/**
* Specifies whether keyboard navigation is enabled
* ([see example]({% slug keyboard_navigation_listview %})).
*
* @default true
*/
set navigable(navigable) {
if (!navigable && isPresent(this.removeNavigationListeners)) {
this.removeNavigationListeners();
this.removeNavigationListeners = null;
this.navigationService.isEnabled = false;
}
else if (navigable && !isPresent(this.removeNavigationListeners)) {
this.addNavigationListeners();
this.navigationService.isEnabled = true;
}
this._navigable = navigable;
}
get navigable() {
return this._navigable;
}
/**
* Specifies the page size used by the ListView pager
* ([more details]({% slug paging_listview %})).
*/
pageSize;
/**
* Defines the number of records to be skipped by the pager
* ([more details]({% slug paging_listview %})).
*/
set skip(skip) {
const parsed = parseInt(skip, 10);
const defaultSkipValue = 0;
this._skip = !isNaN(parsed) ? parsed : defaultSkipValue;
}
get skip() {
return this._skip;
}
/**
* Configures whether the ListView renders a pager
* ([more details]({% slug paging_listview %})).
* When you provide a boolean value, it renders a pager with the default settings.
*/
set pageable(pageable) {
this._pageable = pageable;
this.pagerSettings = pageable ? Object.assign({}, DEFAULT_PAGER_SETTINGS, pageable) : null;
}
get pageable() {
return this._pageable;
}
/**
* Specifies the height (in pixels) of the ListView component.
* When the content height exceeds the component height, a vertical scrollbar renders.
*
* To set the height of the ListView, you can also use `style.height`. The `style.height`
* option supports units such as `px`, `%`, `em`, `rem`, and others.
*/
height;
/**
* Fires when you scroll to the last record on the page
* ([see endless scrolling example]({% slug scrollmodes_listview %}#toc-endless-scrolling)).
*/
scrollBottom = new EventEmitter();
/**
* Fires when you change the page or the page size of the ListView
* ([see example]({% slug paging_listview %}#toc-remote-binding)).
* You have to handle the event yourself and page the data.
*/
pageChange = new EventEmitter();
/**
* Fires when you change the page size of the ListView. You can prevent this event (`$event.preventDefault()`).
* When not prevented, the `pageChange` event fires subsequently.
*/
pageSizeChange = new EventEmitter();
/**
* Fires when you click the **Edit** command button to edit an item
* ([see example]({% slug editing_template_forms_listview %}#toc-editing-records)).
*/
edit = new EventEmitter();
/**
* Fires when you click the **Cancel** command button to close an item
* ([see example]({% slug editing_template_forms_listview %}#toc-cancelling-editing)).
*/
cancel = new EventEmitter();
/**
* Fires when you click the **Save** command button to save changes in an item
* ([see example]({% slug editing_template_forms_listview %}#toc-saving-records)).
*/
save = new EventEmitter();
/**
* Fires when you click the **Remove** command button to remove an item
* ([see example]({% slug editing_template_forms_listview %}#toc-removing-records)).
*/
remove = new EventEmitter();
/**
* Fires when you click the **Add** command button to add a new item
* ([see example]({% slug editing_template_forms_listview %}#toc-adding-records)).
*/
add = new EventEmitter();
/**
* @hidden
*/
pagerSettings;
/**
* @hidden
*
* Gets the data items passed to the ListView.
* If a `ListViewDataResult` is passed, the data value is used. If an array is passed - it's directly used.
*/
get items() {
if (!isPresent(this.data)) {
return [];
}
return Array.isArray(this.data) ? this.data : this.data.data;
}
/**
* @hidden
*
* Gets the total number of records passed to the ListView.
* If a `ListViewDataResult` is passed, the total value is used. If an array is passed - its length is used.
*/
get total() {
if (!this.pageable) {
return;
}
if (!isPresent(this.data)) {
return 0;
}
return Array.isArray(this.data) ? this.data.length : this.data.total;
}
/**
* @hidden
*/
get containerTabindex() {
// workaround for FF, where a scrollable container is focusable even without a tabindex and creates an unwanted tab stop
// https://bugzilla.mozilla.org/show_bug.cgi?id=616594
return this.navigable ? -1 : null;
}
/**
* Gets the current active item index
* ([see example]({% slug keyboard_navigation_listview %}#toc-controlling-the-focus)).
* Returns `null` when keyboard navigation is disabled.
*/
get activeIndex() {
return this.navigationService.activeIndex;
}
removeNavigationListeners;
_skip = 0;
_navigable = true;
_pageable;
lastScrollTop;
_data;
editServiceSubscription;
constructor(ngZone, element, renderer, changeDetectorRef, editService, navigationService) {
this.ngZone = ngZone;
this.element = element;
this.renderer = renderer;
this.changeDetectorRef = changeDetectorRef;
this.editService = editService;
this.navigationService = navigationService;
validatePackage(packageMetadata);
this.attachEditHandlers();
}
ngAfterViewInit() {
this.lastScrollTop = this.contentContainer?.nativeElement.scrollTop;
}
ngOnChanges(changes) {
if (isChanged('height', changes, false)) {
this.renderer.setStyle(this.element.nativeElement, 'height', `${this.height}px`);
}
}
ngOnDestroy() {
if (isPresent(this.editServiceSubscription)) {
this.editServiceSubscription.unsubscribe();
}
}
/**
* @hidden
*/
templateContext(index) {
return {
"$implicit": this.items[index],
"isLast": index === this.items.length - 1,
"isFirst": index === 0,
"dataItem": this.items[index],
"index": index
};
}
/**
* @hidden
*/
editTemplateContext(index) {
const isNew = index === -1;
const group = isNew ? this.editService.newItemGroup : this.editService.editGroup(index);
return {
"$implicit": group,
"formGroup": group,
"dataItem": isNew ? this.editService.newDataItem : this.items[index],
"isNew": isNew,
"index": index
};
}
/**
* Focuses the item at the specified index ([see example]({% slug keyboard_navigation_listview %}#toc-controlling-the-focus)):
* - When you specify no index, the current active index receives focus.
* - When the passed value is below `0`, the first item receives focus.
* - When the passed value is above the last available index, the last item receives focus.
*
* > The `index` parameter is based on the logical structure of the ListView and does not correspond to the data item index&mdash
* > the index `0` corresponds to the first rendered list item. Paging is not taken into account.
* > Also, the `navigable` property must first be set to `true` for the method to work as expected.
*/
focus(index) {
const totalRenderedItems = this.listViewItems.length;
this.navigationService.focusIndex(index, totalRenderedItems);
}
/**
* Creates a new item editor ([see example]({% slug editing_template_forms_listview %}#toc-adding-records)).
*
* @param {FormGroup} group - The [`FormGroup`](link:site.data.urls.angular['formgroupapi']) that describes
* the edit form. When called with a data item, it builds the `FormGroup` from the data item fields.
*/
addItem(group) {
const isFormGroup = group instanceof FormGroup;
if (!isFormGroup) {
const fields = Object.keys(group).reduce(createControl(group), {});
group = new FormGroup(fields);
}
this.editService.addItem(group);
}
/**
* Switches the specified item to edit mode ([see example]({% slug editing_template_forms_listview %}#toc-editing-records)).
*
* @param index - The item index that switches to edit mode.
* @param group - The [`FormGroup`](link:site.data.urls.angular['formgroupapi'])
* that describes the edit form.
*/
editItem(index, group) {
this.editService.editItem(index, group);
this.changeDetectorRef.markForCheck();
}
/**
* Closes the editor for a given item ([see example]({% slug editing_template_forms_listview %}#toc-cancelling-editing)).
*
* @param {number} index - The item index that switches out of the edit mode. When you provide no index, the editor of the new item will close.
*/
closeItem(index) {
this.editService.close(index);
this.changeDetectorRef.markForCheck();
}
/**
* @hidden
*/
isEdited(index) {
return this.editService.isEdited(index);
}
/**
* @hidden
*/
handlePageChange(event) {
this.scrollToContainerTop();
const firstIndex = 0;
this.navigationService.setActiveIndex(firstIndex);
this.pageChange.emit(event);
}
/**
* @hidden
*/
handleContentScroll = () => {
if (!hasObservers(this.scrollBottom)) {
return;
}
const THRESHOLD = 2;
const { scrollHeight, scrollTop, clientHeight } = this.contentContainer.nativeElement;
const isScrollUp = this.lastScrollTop > scrollTop;
this.lastScrollTop = scrollTop;
if (isScrollUp) {
return;
}
const atBottom = scrollHeight - clientHeight - scrollTop <= THRESHOLD;
if (atBottom) {
this.ngZone.run(() => {
const event = { sender: this };
this.scrollBottom.emit(event);
});
}
};
/**
* @hidden
*/
itemPosInSet(index) {
if (!this.pageable) {
return;
}
// adds 1 as the aria-posinset is not zero-based and the counting starts from 1
return this.skip + index + 1;
}
scrollToContainerTop() {
const container = this.contentContainer.nativeElement;
container.scrollTop = 0;
container.scrollLeft = 0;
}
addNavigationListeners() {
this.ngZone.runOutsideAngular(() => {
const removeKeydownListener = this.renderer.listen(this.contentContainer.nativeElement, 'keydown', event => this.navigationService.handleKeyDown(event, this.listViewItems.length));
const removeFocusInListener = this.renderer.listen(this.contentContainer.nativeElement, 'focusin', event => this.navigationService.handleFocusIn(event));
const removeFocusOutListener = this.renderer.listen(this.contentContainer.nativeElement, 'focusout', event => this.navigationService.handleFocusOut(event));
this.removeNavigationListeners = () => {
removeKeydownListener();
removeFocusInListener();
removeFocusOutListener();
};
});
}
attachEditHandlers() {
if (!isPresent(this.editService)) {
return;
}
this.editServiceSubscription = this.editService
.changes.subscribe(this.emitCRUDEvent.bind(this));
}
emitCRUDEvent(args) {
const { action, itemIndex, formGroup } = args;
let dataItem = this.items[itemIndex];
if (action !== 'add' && formGroup) {
dataItem = formGroup.value;
}
Object.assign(args, {
dataItem: dataItem,
sender: this
});
this[action].emit(args);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListViewComponent, deps: [{ token: i0.NgZone }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: EditService }, { token: NavigationService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ListViewComponent, isStandalone: true, selector: "kendo-listview", inputs: { bordered: "bordered", data: "data", loading: "loading", containerStyle: "containerStyle", itemStyle: "itemStyle", containerClass: "containerClass", itemClass: "itemClass", containerLabel: "containerLabel", containerRole: "containerRole", listItemRole: "listItemRole", navigable: "navigable", pageSize: "pageSize", skip: "skip", pageable: "pageable", height: "height" }, outputs: { scrollBottom: "scrollBottom", pageChange: "pageChange", pageSizeChange: "pageSizeChange", edit: "edit", cancel: "cancel", save: "save", remove: "remove", add: "add" }, host: { properties: { "class.k-listview": "this.className", "class.k-listview-bordered": "this.bordered" } }, providers: [
EditService,
NavigationService,
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.listview'
}
], queries: [{ propertyName: "itemTemplate", first: true, predicate: ItemTemplateDirective, descendants: true }, { propertyName: "headerTemplate", first: true, predicate: HeaderTemplateDirective, descendants: true }, { propertyName: "footerTemplate", first: true, predicate: FooterTemplateDirective, descendants: true }, { propertyName: "loaderTemplate", first: true, predicate: LoaderTemplateDirective, descendants: true }, { propertyName: "editTemplate", first: true, predicate: EditTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "contentContainer", first: true, predicate: ["contentContainer"], descendants: true, static: true }, { propertyName: "listViewItems", predicate: ListViewNavigableItemDirective, descendants: true }], exportAs: ["kendoListView"], usesOnChanges: true, ngImport: i0, template: `
<!-- top pager -->
@if (pagerSettings?.position !== 'bottom') {
<ng-template
[ngTemplateOutlet]="pagerTemplate"
[ngTemplateOutletContext]="{ pagerClass: 'k-listview-pager k-listview-pager-top' }"
>
</ng-template>
}
<!-- header -->
@if (headerTemplate) {
<div
class="k-listview-header"
>
<ng-template
[ngTemplateOutlet]="headerTemplate?.templateRef"
>
</ng-template>
</div>
}
<!-- content -->
<div
#contentContainer
[attr.tabindex]="containerTabindex"
class="k-listview-content"
[ngClass]="containerClass"
[ngStyle]="containerStyle"
[kendoEventsOutsideAngular]="{
scroll: handleContentScroll
}"
[scope]="this"
[attr.role]="containerRole"
[attr.aria-label]="containerLabel"
>
<!-- new item edit template -->
@if (editService.hasNewItem) {
<div
class="k-listview-item"
[attr.role]="listItemRole"
kendoListViewNavigableItem
[index]="-1"
[attr.data-kendo-listview-item-index]="-1"
[ngClass]="itemClass"
[ngStyle]="itemStyle"
>
@if (editTemplate) {
<ng-template
[ngTemplateOutlet]="editTemplate?.templateRef"
[ngTemplateOutletContext]="editTemplateContext(-1)"
>
</ng-template>
}
</div>
}
<!-- items -->
@for (dataItem of items; track dataItem; let index = $index; let first = $first; let last = $last) {
<div
class="k-listview-item"
[attr.role]="listItemRole"
[attr.aria-posinset]="itemPosInSet(index)"
[attr.aria-setsize]="total"
kendoListViewNavigableItem
[index]="index"
[attr.data-kendo-listview-item-index]="index"
[ngClass]="itemClass"
[ngStyle]="itemStyle"
>
<ng-template
[ngTemplateOutlet]="isEdited(index) ? editTemplate?.templateRef : itemTemplate?.templateRef"
[ngTemplateOutletContext]="isEdited(index) ? editTemplateContext(index) : templateContext(index)"
>
</ng-template>
</div>
}
</div>
<!-- loading indicator -->
@if (loading && !loaderTemplate) {
<div
class="k-loading-mask"
>
<!-- TODO: the k-loading-text is hidden with css but read by readers - review when implementing accessibility + possible localization case -->
<span class="k-loading-text">Loading</span>
<div class="k-loading-image"></div>
<div class="k-loading-color"></div>
</div>
}
@if (loading && loaderTemplate) {
<ng-template
[ngTemplateOutlet]="loaderTemplate.templateRef"
>
</ng-template>
}
<!-- footer -->
@if (footerTemplate) {
<div
class="k-listview-footer"
>
<ng-template
[ngTemplateOutlet]="footerTemplate?.templateRef"
>
</ng-template>
</div>
}
<!-- bottom pager -->
@if (pagerSettings?.position !== 'top') {
<ng-template
[ngTemplateOutlet]="pagerTemplate"
[ngTemplateOutletContext]="{ pagerClass: 'k-listview-pager' }"
>
</ng-template>
}
<!-- pager template -->
<ng-template #pagerTemplate let-pagerClass="pagerClass">
@if (pageable) {
<kendo-datapager
[class]="pagerClass"
[total]="total"
[pageSize]="pageSize"
[skip]="skip"
[buttonCount]="pagerSettings.buttonCount"
[info]="pagerSettings.info"
[previousNext]="pagerSettings.previousNext"
[type]="pagerSettings.type"
[pageSizeValues]="pagerSettings.pageSizeValues"
(pageChange)="handlePageChange($event)"
(pageSizeChange)="pageSizeChange.emit($event)"
>
</kendo-datapager>
}
</ng-template>
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }, { kind: "directive", type: ListViewNavigableItemDirective, selector: "[kendoListViewNavigableItem]", inputs: ["index"] }, { kind: "component", type: PagerComponent, selector: "kendo-datapager, kendo-pager", inputs: ["externalTemplate", "total", "skip", "pageSize", "buttonCount", "info", "type", "pageSizeValues", "previousNext", "navigable", "size", "responsive", "adaptiveMode"], outputs: ["pageChange", "pageSizeChange", "pagerInputVisibilityChange", "pageTextVisibilityChange", "itemsTextVisibilityChange"], exportAs: ["kendoDataPager", "kendoPager"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListViewComponent, decorators: [{
type: Component,
args: [{
changeDetection: ChangeDetectionStrategy.OnPush,
exportAs: 'kendoListView',
selector: 'kendo-listview',
providers: [
EditService,
NavigationService,
LocalizationService