UNPKG

@rxap/data-source

Version:

Provides a set of classes and decorators for creating and managing data sources in Angular applications, including base classes, static data sources, observable data sources, and method data sources. It also includes a component for displaying data source

536 lines (530 loc) 23.4 kB
import * as i0 from '@angular/core'; import { EventEmitter, isDevMode, TemplateRef, ViewContainerRef, INJECTOR, IterableDiffers, ChangeDetectorRef, NgZone, Directive, Inject, Input, Output } from '@angular/core'; import * as i1 from '@rxap/data-source'; import { BaseDataSource, DataSourceLoader } from '@rxap/data-source'; import { Subscription } from 'rxjs'; import { take, tap, filter, distinctUntilChanged } from 'rxjs/operators'; import { noop } from '@rxap/utilities'; class DataSourceCollectionTemplateContext { constructor($implicit, connection$, index, count) { this.$implicit = $implicit; this.connection$ = connection$; this.index = index; this.count = count; } get first() { return this.index === 0; } get last() { return this.index === this.count - 1; } get even() { return this.index % 2 === 0; } get odd() { return !this.even; } } function getTypeName(type) { return type['name'] || typeof type; } class RecordViewTuple { constructor(record, view) { this.record = record; this.view = view; } } class DataSourceCollectionDirective { constructor(dataSourceLoader, template, viewContainerRef, injector, differs, cdr, zone) { this.dataSourceLoader = dataSourceLoader; this.template = template; this.viewContainerRef = viewContainerRef; this.injector = injector; this.differs = differs; this.cdr = cdr; this.zone = zone; this.viewer = { id: '[rxapDataSourceCollection]' }; this.dataSource = null; this.loaded = new EventEmitter(); this.error = new EventEmitter(); this.connection$ = null; this.subscription = new Subscription(); this._differ = null; this._dirty = true; /** * Idecates that the data source returned a empty collection * * true - is empty * false - is NOT empty * null - unknown * * @private */ this._empty = null; /** * Holds the empty template view ref. * * Is used to determine if a empty template is added to the * view. And used to destruct the empty template if the data source * changes from empty to not empty. * * @private */ this._emptyTemplateViewRef = null; this._dataSourceLoadingSubscription = null; /** * Holds the data that should be displayed * @private */ this._data = null; } /** * A function that defines how to track changes for items in the iterable. * * When items are added, moved, or removed in the iterable, * the directive must re-render the appropriate DOM nodes. * To minimize churn in the DOM, only nodes that have changed * are re-rendered. * * By default, the change detector assumes that * the object instance identifies the node in the iterable. * When this function is supplied, the directive uses * the result of calling this function to identify the item node, * rather than the identity of the object itself. * * The function receives two inputs, * the iteration index and the node object ID. */ set rxapDataSourceCollectionTrackBy(fn) { if (isDevMode() && fn != null && typeof fn !== 'function') { // TODO(vicb): use a log service once there is a public one available if (console && console.warn) { console.warn(`trackBy must be a function, but received ${JSON.stringify(fn)}. ` + `See https://angular.io/api/common/NgForOf#change-propagation for more information.`); } } this._trackByFn = fn; } get ngForTrackBy() { return this._trackByFn; } set data(data) { this._data = data; this._dirty = true; } /** * Asserts the correct type of the context for the template that `NgForOf` will render. * * The presence of this method is a signal to the Ivy template type-check compiler that the * `NgForOf` structural directive renders its template with a specific context type. */ static ngTemplateContextGuard(dir, ctx) { return true; } // TODO : handel that case: a new data source instance is provided and an open connection to the old data source exists. ngOnChanges(changes) { const dataSourceOrIdOrTokenChange = changes['dataSourceOrIdOrToken']; if (dataSourceOrIdOrTokenChange) { this.dataSource = this.loadDataSource(); // Dont connect to the data source on the first change. // Else the parent/sibling components are not initialized // and the parameters from the parent/sibling components can not // be used in the connect logic. // Example: The PaginationDataSource required the pageSize, but the pageSize // in the MatPaginator will be set after the ngOnChanges logic is called. // So the initial pageSize is undefined. if (!dataSourceOrIdOrTokenChange.firstChange) { this.connect(); } } } ngAfterViewInit() { this.connect(); } ngOnDestroy() { this._differ = null; this._empty = null; this.viewContainerRef.clear(); this.dataSource?.disconnect(this.viewer); this.subscription.unsubscribe(); this._dataSourceLoadingSubscription?.unsubscribe(); } /** * Applies the changes when needed. */ ngDoCheck() { if (this._empty === true) { this.viewContainerRef.clear(); this._differ = null; // attaches the empty template if the data source is empty if (this.emptyTemplate) { this._emptyTemplateViewRef = this.viewContainerRef.createEmbeddedView(this.emptyTemplate); } } else if (this._empty === false) { // detach and destroy the empty template if the data source is not // empty any more if (this._emptyTemplateViewRef) { this._emptyTemplateViewRef.detach(); this._emptyTemplateViewRef.destroy(); this._emptyTemplateViewRef = null; } if (this._data) { this._dirty = false; // React on ngForOf changes only once all inputs have been initialized const value = this._data; if (!this._differ && value) { try { this._differ = this.differs.find(value).create(this.ngForTrackBy); } catch { throw new Error(`Cannot find a differ supporting object '${value}' of type '${getTypeName(value)}'. NgFor only supports binding to Iterables such as Arrays.`); } } } if (this._differ) { const changes = this._differ.diff(this._data); if (changes) { this.applyChanges(changes); } } } } embedErrorTemplate(error) { if (!this.errorTemplate) { if (isDevMode()) { console.log('Skip error template embedding. ErrorTemplate is not defined'); } return; } const context = { $implicit: error, // eslint-disable-next-line @typescript-eslint/no-empty-function refresh: this.dataSource?.refresh.bind(this.dataSource) ?? (() => { }), }; this.embeddedErrorViewRef?.destroy(); this.viewContainerRef.clear(); this.embeddedErrorViewRef = this.viewContainerRef.createEmbeddedView(this.errorTemplate, context); this.cdr.detectChanges(); } connect() { if (this.dataSource) { this.connection$ = this.dataSource.connect(this.viewer); this.zone.onStable .pipe(take(1), tap(() => { this.zone.run(() => { this.subscription.add(this.connection$.pipe(tap({ next: (response) => { this._empty = response.length === 0; this._data = response; this.cdr.detectChanges(); }, error: (error) => { this.error.emit(error); console.error(`Connection failure in ${this.dataSource.constructor.name}: ${error.message}`, error); this.embedErrorTemplate(error); }, })).subscribe()); }); })) .subscribe(); } else { throw new Error('Can not connect to the data source. The data source is not loaded!'); } } applyChanges(changes) { const insertTuples = []; changes.forEachOperation((item, adjustedPreviousIndex, currentIndex) => { if (item.previousIndex == null) { // NgForOf is never "null" or "undefined" here because the differ detected // that a new item needs to be inserted from the iterable. This implies that // there is an iterable value for "_ngForOf". const view = this.viewContainerRef.createEmbeddedView(this.template, new DataSourceCollectionTemplateContext(null, this.connection$, -1, -1), currentIndex === null ? undefined : currentIndex); const tuple = new RecordViewTuple(item, view); insertTuples.push(tuple); } else if (currentIndex == null) { this.viewContainerRef.remove(adjustedPreviousIndex === null ? undefined : adjustedPreviousIndex); } else if (adjustedPreviousIndex !== null) { const view = this.viewContainerRef.get(adjustedPreviousIndex); this.viewContainerRef.move(view, currentIndex); const tuple = new RecordViewTuple(item, view); insertTuples.push(tuple); } }); for (let i = 0; i < insertTuples.length; i++) { this.perViewChange(insertTuples[i].view, insertTuples[i].record); } for (let i = 0, ilen = this.viewContainerRef.length; i < ilen; i++) { const viewRef = this.viewContainerRef.get(i); viewRef.context.index = i; viewRef.context.count = ilen; viewRef.context.connection$ = this.connection$; } changes.forEachIdentityChange((record) => { const viewRef = this.viewContainerRef.get(record.currentIndex); viewRef.context.$implicit = record.item; }); } loadDataSource() { let dataSource = null; if (typeof this.dataSourceOrIdOrToken === 'string') { dataSource = this.dataSourceLoader.load(this.dataSourceOrIdOrToken, undefined, this.injector); } else if (this.dataSourceOrIdOrToken instanceof BaseDataSource) { dataSource = this.dataSourceOrIdOrToken; } else if (this.dataSourceOrIdOrToken !== null) { dataSource = this.injector.get(this.dataSourceOrIdOrToken); } this._dataSourceLoadingSubscription?.unsubscribe(); this._dataSourceLoadingSubscription = dataSource?.loading$.pipe(filter(Boolean)).subscribe(this.loaded) ?? null; return dataSource; } perViewChange(view, record) { view.context.$implicit = record.item; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.16", ngImport: i0, type: DataSourceCollectionDirective, deps: [{ token: DataSourceLoader }, { token: TemplateRef }, { token: ViewContainerRef }, { token: INJECTOR }, { token: IterableDiffers }, { token: ChangeDetectorRef }, { token: NgZone }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.16", type: DataSourceCollectionDirective, isStandalone: true, selector: "[rxapDataSourceCollection]", inputs: { dataSourceOrIdOrToken: ["rxapDataSourceCollectionFrom", "dataSourceOrIdOrToken"], viewer: ["rxapDataSourceCollectionViewer", "viewer"], emptyTemplate: ["rxapDataSourceCollectionEmpty", "emptyTemplate"], errorTemplate: ["rxapDataSourceCollectionErrorTemplate", "errorTemplate"], rxapDataSourceCollectionTrackBy: "rxapDataSourceCollectionTrackBy" }, outputs: { loaded: "loaded", error: "error" }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.16", ngImport: i0, type: DataSourceCollectionDirective, decorators: [{ type: Directive, args: [{ selector: '[rxapDataSourceCollection]', standalone: true, }] }], ctorParameters: () => [{ type: i1.DataSourceLoader, decorators: [{ type: Inject, args: [DataSourceLoader] }] }, { type: i0.TemplateRef, decorators: [{ type: Inject, args: [TemplateRef] }] }, { type: i0.ViewContainerRef, decorators: [{ type: Inject, args: [ViewContainerRef] }] }, { type: undefined, decorators: [{ type: Inject, args: [INJECTOR] }] }, { type: i0.IterableDiffers, decorators: [{ type: Inject, args: [IterableDiffers] }] }, { type: i0.ChangeDetectorRef, decorators: [{ type: Inject, args: [ChangeDetectorRef] }] }, { type: i0.NgZone, decorators: [{ type: Inject, args: [NgZone] }] }], propDecorators: { dataSourceOrIdOrToken: [{ type: Input, args: [{ required: true, alias: 'rxapDataSourceCollectionFrom', }] }], viewer: [{ type: Input, args: ['rxapDataSourceCollectionViewer'] }], emptyTemplate: [{ type: Input, args: ['rxapDataSourceCollectionEmpty'] }], errorTemplate: [{ type: Input, args: ['rxapDataSourceCollectionErrorTemplate'] }], loaded: [{ type: Output }], error: [{ type: Output }], rxapDataSourceCollectionTrackBy: [{ type: Input }] } }); class DataSourceDirective { /** * Asserts the correct type of the context for the template that `NgForOf` will render. * * The presence of this method is a signal to the Ivy template type-check compiler that the * `NgForOf` structural directive renders its template with a specific context type. */ static ngTemplateContextGuard(dir, ctx) { return true; } constructor(dataSourceLoader, template, viewContainerRef, injector, cdr, zone) { this.dataSourceLoader = dataSourceLoader; this.template = template; this.viewContainerRef = viewContainerRef; this.injector = injector; this.cdr = cdr; this.zone = zone; this.embedded = new EventEmitter(); this.loaded = new EventEmitter(); this.error = new EventEmitter(); this.dataSource = null; /** * @deprecated removed * @protected */ this.subscription = new Subscription(); this.embeddedViewRef = null; this.embeddedErrorViewRef = null; this._dataSourceLoadingSubscription = null; this._dataSourceConnectionSubscription = null; this.viewer = this; } ngOnChanges(changes) { const dataSourceOrIdOrTokenChange = changes['dataSourceOrIdOrToken']; if (dataSourceOrIdOrTokenChange) { this.dataSource = this.loadDataSource(); // Dont connect to the data source on the first change. // Else the parent/sibling components are not initialized // and the parameters from the parent/sibling components can not // be used in the connect logic. // Example: The PaginationDataSource required the pageSize, but the pageSize // in the MatPaginator will be set after the ngOnChanges logic is called. // So the initial pageSize is undefined. if (!dataSourceOrIdOrTokenChange.firstChange) { this.connect(); } } } ngAfterViewInit() { this.connect(); } embedErrorTemplate(error) { if (!this.errorTemplate) { if (isDevMode()) { console.log('Skip error template embedding. ErrorTemplate is not defined'); } return; } const context = { $implicit: error, // eslint-disable-next-line @typescript-eslint/no-empty-function refresh: this.dataSource?.refresh.bind(this.dataSource) ?? noop, }; this.embeddedViewRef?.destroy(); this.embeddedViewRef = null; this.embeddedErrorViewRef?.destroy(); this.embeddedErrorViewRef = this.viewContainerRef.createEmbeddedView(this.errorTemplate, context); this.cdr.detectChanges(); } embedTemplate(response) { this.embeddedErrorViewRef?.destroy(); this.embeddedErrorViewRef = null; const context = { $implicit: response, connection$: this.connection$, }; if (this.embeddedViewRef && !this.hasChanged(this.embeddedViewRef.context.$implicit, response)) { this.embeddedViewRef.context = context; } else { this.embeddedViewRef?.destroy(); this.embeddedViewRef = this.viewContainerRef.createEmbeddedView(this.template, context); } this.embedded.emit(response); this.cdr.detectChanges(); } ngOnDestroy() { this.dataSource?.disconnect(this.viewer); this.subscription.unsubscribe(); this._dataSourceConnectionSubscription?.unsubscribe(); this._dataSourceLoadingSubscription?.unsubscribe(); } loadDataSource() { let dataSource = null; if (typeof this.dataSourceOrIdOrToken === 'string') { dataSource = this.dataSourceLoader.load(this.dataSourceOrIdOrToken, undefined, this.injector); } else if (this.dataSourceOrIdOrToken instanceof BaseDataSource) { dataSource = this.dataSourceOrIdOrToken; } else if (this.dataSourceOrIdOrToken !== null) { dataSource = this.injector.get(this.dataSourceOrIdOrToken); } this._dataSourceLoadingSubscription?.unsubscribe(); this._dataSourceLoadingSubscription = dataSource?.loading$.pipe(filter(Boolean)).subscribe(this.loaded) ?? null; return dataSource; } connect() { if (this.dataSource) { this.dataSource.hasError$.pipe(distinctUntilChanged(), tap(hasError => { if (hasError) { this.embedErrorTemplate(null); } else { this.embeddedErrorViewRef?.destroy(); this.embeddedErrorViewRef = null; } })).subscribe(); this.connection$ = this.dataSource.connect(this.viewer); this.zone.onStable .pipe(take(1), tap(() => { this.zone.run(() => { this._dataSourceConnectionSubscription?.unsubscribe(); this._dataSourceConnectionSubscription = this.connection$ .pipe(tap({ next: (response) => { this.embedTemplate(response); }, error: (error) => { this.error.emit(error); console.error(`Connection failure in ${this.dataSource.constructor.name}: ${error.message}`, error); this.embedErrorTemplate(error); }, })) .subscribe(); }); })) .subscribe(); } } hasChanged(last, current) { const lastKey = this.trackBy ? this.trackBy(last) : last; const currentKey = this.trackBy ? this.trackBy(current) : current; return lastKey !== currentKey; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.16", ngImport: i0, type: DataSourceDirective, deps: [{ token: i1.DataSourceLoader }, { token: i0.TemplateRef }, { token: i0.ViewContainerRef }, { token: INJECTOR }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.16", type: DataSourceDirective, isStandalone: true, selector: "[rxapDataSource]", inputs: { dataSourceOrIdOrToken: ["rxapDataSourceFrom", "dataSourceOrIdOrToken"], viewer: ["rxapDataSourceViewer", "viewer"], errorTemplate: ["rxapDataSourceErrorTemplate", "errorTemplate"], trackBy: ["rxapDataSourceTrackBy", "trackBy"] }, outputs: { embedded: "embedded", loaded: "loaded", error: "error" }, exportAs: ["rxapDataSource"], usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.16", ngImport: i0, type: DataSourceDirective, decorators: [{ type: Directive, args: [{ selector: '[rxapDataSource]', exportAs: 'rxapDataSource', standalone: true, }] }], ctorParameters: () => [{ type: i1.DataSourceLoader }, { type: i0.TemplateRef }, { type: i0.ViewContainerRef }, { type: undefined, decorators: [{ type: Inject, args: [INJECTOR] }] }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }], propDecorators: { dataSourceOrIdOrToken: [{ type: Input, args: [{ required: true, alias: 'rxapDataSourceFrom', }] }], viewer: [{ type: Input, args: ['rxapDataSourceViewer'] }], errorTemplate: [{ type: Input, args: ['rxapDataSourceErrorTemplate'] }], embedded: [{ type: Output }], loaded: [{ type: Output }], error: [{ type: Output }], trackBy: [{ type: Input, args: ['rxapDataSourceTrackBy'] }] } }); // region // endregion /** * Generated bundle index. Do not edit. */ export { DataSourceCollectionDirective, DataSourceCollectionTemplateContext, DataSourceDirective }; //# sourceMappingURL=rxap-data-source-directive.mjs.map