@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
JavaScript
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.1", 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.1", 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.1", 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.1", 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.1", 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.1", 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