UNPKG

@nativescript-community/ui-collectionview

Version:

Allows you to easily add a collection view (grid list view) to your projects. Supports vertical and horizontal modes, templating, and more.

455 lines (450 loc) 18.7 kB
import * as i0 from '@angular/core'; import { EventEmitter, ElementRef, IterableDiffers, NgZone, ViewContainerRef, TemplateRef, NO_ERRORS_SCHEMA, ChangeDetectionStrategy, Component, Inject, HostListener, Input, ContentChild, Output, ViewChild, Directive, Host, NgModule } from '@angular/core'; import { CollectionView, ListViewViewTypes, CLog, CLogTypes } from '@nativescript-community/ui-collectionview'; import * as i1 from '@nativescript/angular'; import { registerElement, DetachedLoader, isListLikeIterable, extractSingleViewRecursive, NativeScriptRendererFactory } from '@nativescript/angular'; import { ObservableArray, Trace, LayoutBase } from '@nativescript/core'; const _c0 = ["loader"]; registerElement('CollectionView', () => CollectionView); const NG_VIEW = '_ngViewRef'; class ItemContext { $implicit; item; index; even; odd; constructor($implicit, item, index, even, odd) { this.$implicit = $implicit; this.item = item; this.index = index; this.even = even; this.odd = odd; } } class CollectionViewComponent { _elementRef; _iterableDiffers; _renderer; _ngZone; get nativeElement() { return this._collectionView; } get listView() { return this._collectionView; } loader; setupItemView = new EventEmitter(); itemTemplateQuery; autoReuseViews = false; detachedLoaderFactory() { const ref = this.loader.createComponent(DetachedLoader, { index: 0 }); this.loader.detach(0); this._loaders.push(ref); return ref; } get itemTemplate() { return this._itemTemplate; } set itemTemplate(value) { this._itemTemplate = value; this._collectionView.refresh(); } get items() { return this._items; } set items(value) { this._items = value; let needDiffer = true; if (value instanceof ObservableArray) { needDiffer = false; } if (needDiffer && !this._differ && isListLikeIterable(value)) { this._differ = this._iterableDiffers.find(this._items).create((_index, item) => item); } this._collectionView.items = this._items; } _collectionView; _items; _differ; _itemTemplate; _templateMap; _loaders; constructor(_elementRef, _iterableDiffers, _renderer, _ngZone) { this._elementRef = _elementRef; this._iterableDiffers = _iterableDiffers; this._renderer = _renderer; this._ngZone = _ngZone; this._collectionView = _elementRef.nativeElement; this._collectionView.on(CollectionView.itemLoadingEvent, this.onItemLoading, this); this._collectionView.itemViewLoader = this.itemViewLoader; this._loaders = []; } itemViewLoader = (viewType) => this._ngZone.run(() => { switch (viewType) { case ListViewViewTypes.ItemView: if (this._itemTemplate && this.loader) { const typedView = this.getOrCreate(this._itemTemplate); return typedView; } break; } return null; }); ngAfterContentInit() { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'CollectionView.ngAfterContentInit()'); } this.setItemTemplates(); } ngOnDestroy() { this._collectionView.off(CollectionView.itemLoadingEvent, this.onItemLoading, this); this._collectionView = null; this._loaders.forEach((l) => l.destroy()); this._loaders = null; this.viewToLoader = null; this.viewToTemplate = null; this.viewPool = null; this._items = null; this._differ = null; this._itemTemplate = null; if (this._templateMap) { this._templateMap.clear(); } this._templateMap = null; } ngDoCheck() { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'ngDoCheck() - execute differ? ' + this._differ); } if (this._differ) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'ngDoCheck() - execute differ'); } const changes = this._differ.diff(this._items); if (changes) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'ngDoCheck() - refresh'); } this.refresh(); } } } registerTemplate(key, template) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'registerTemplate for key: ' + key); } if (!this._templateMap) { this._templateMap = new Map(); } const keyedTemplate = { key, createView: this.getItemTemplateViewFactory(template) }; this._templateMap.set(key, keyedTemplate); } onItemLoading(args) { if (!args.view && !this.itemTemplate) { return; } if (!this.items) return; const index = args.index; const items = args.object.items; const currentItem = typeof items.getItem === 'function' ? items.getItem(index) : items[index]; let viewRef; if (Trace.isEnabled()) { CLog(CLogTypes.info, `onItemLoading: ${index} - Reusing existing view`); } viewRef = args.view[NG_VIEW]; // Getting angular view from original element (in cases when ProxyViewContainer // is used NativeScript internally wraps it in a StackLayout) if (!viewRef && args.view instanceof LayoutBase && args.view.getChildrenCount() > 0) { viewRef = args.view.getChildAt(0)[NG_VIEW]; } if (!viewRef && Trace.isEnabled()) { if (Trace.isEnabled()) { CLog(CLogTypes.info, `ViewReference not found for item ${index}. View recycling is not working`); } } if (!viewRef) { if (Trace.isEnabled()) { CLog(CLogTypes.info, `onItemLoading: ${index} - Creating view from template`); } viewRef = this.loader.createEmbeddedView(this.itemTemplate, new ItemContext(), 0); args.view = getItemViewRoot(viewRef); args.view[NG_VIEW] = viewRef; } this.setupViewRef(viewRef, currentItem, index); this.detectChangesOnChild(viewRef, index); } onItemRecyclingInternal(args) { if (!args.view) { return; } let ngView = args.view[NG_VIEW]; // Getting angular view from original element (in cases when ProxyViewContainer // is used NativeScript internally wraps it in a StackLayout) if (!ngView && args.view instanceof LayoutBase && args.view.getChildrenCount() > 0) { ngView = args.view.getChildAt(0)[NG_VIEW]; } // console.log('recycling', args.view); if (ngView) { ngView.detach(); } } onItemDisposingInternal(args) { if (!args.view) { return; } let ngView = args.view[NG_VIEW]; // Getting angular view from original element (in cases when ProxyViewContainer // is used NativeScript internally wraps it in a StackLayout) if (!ngView && args.view instanceof LayoutBase && args.view.getChildrenCount() > 0) { ngView = args.view.getChildAt(0)[NG_VIEW]; } if (ngView) { ngView.detach(); this.storeViewRef(ngView); } } setupViewRef(view, data, index) { const context = view.context; context.$implicit = data; context.item = data; context.index = index; context.even = index % 2 === 0; context.odd = !context.even; this.setupItemView.next({ context, data, index, view }); } getItemTemplateViewFactory(template) { return () => { const viewRef = this.loader.createEmbeddedView(template, new ItemContext(), 0); const resultView = getItemViewRoot(viewRef); resultView[NG_VIEW] = viewRef; return resultView; }; } viewPool = new Map(); storeViewRef(viewRef) { const templateRef = this.viewToTemplate.get(viewRef); if (templateRef) { const scrap = this.viewPool.get(templateRef); if (scrap) { if (scrap.scrapHead.size >= scrap.scrapSize) { viewRef.destroy(); this.viewToLoader.get(viewRef)?.destroy(); } else { scrap.scrapHead.add(viewRef); } } } } viewToTemplate = new WeakMap(); viewToLoader = new WeakMap(); getOrCreate(templateRef) { return this._ngZone.run(() => { let viewRef = this.getView(templateRef); if (!viewRef) { const loader = this.detachedLoaderFactory(); // viewRef = this.loader.createEmbeddedView(templateRef, new ItemContext(), 0); viewRef = loader.instance.vc.createEmbeddedView(templateRef, new ItemContext(), 0); this.viewToLoader.set(viewRef, loader); this.viewToTemplate.set(viewRef, templateRef); } viewRef.detach(); const resultView = getItemViewRoot(viewRef); resultView[NG_VIEW] = viewRef; resultView.reusable = this.autoReuseViews; return resultView; }); } getView(templateRef) { const pool = this.getViewPool(templateRef); while (pool.scrapHead.size > 0) { const viewRef = pool.scrapHead.values().next().value; pool.scrapHead.delete(viewRef); if (!viewRef.destroyed) { return viewRef; } } return null; } getViewPool(templateRef) { if (!this.viewPool.has(templateRef)) { this.viewPool.set(templateRef, { scrapSize: this.autoReuseViews ? Infinity : 0, scrapHead: new Set() }); } return this.viewPool.get(templateRef); } setItemTemplates() { // The itemTemplateQuery may be changed after list items are added that contain <template> inside, // so cache and use only the original template to avoid errors. this.itemTemplate = this.itemTemplateQuery; if (this._templateMap) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'Setting templates'); } const templates = []; this._templateMap.forEach((value) => { templates.push(value); }); this._collectionView.itemTemplates = templates; } else { // If the map was not initialized this means that there are no named templates, so we register the default one. this._collectionView.itemTemplate = this.getItemTemplateViewFactory(this.itemTemplate); } } detectChangesOnChild(viewRef, index) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'Manually detect changes in child: ' + index); } viewRef.markForCheck(); viewRef.detectChanges(); } refresh() { if (this._collectionView) { this._collectionView.refresh(); } } static ɵfac = function CollectionViewComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || CollectionViewComponent)(i0.ɵɵdirectiveInject(ElementRef), i0.ɵɵdirectiveInject(IterableDiffers), i0.ɵɵdirectiveInject(NativeScriptRendererFactory), i0.ɵɵdirectiveInject(NgZone)); }; static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: CollectionViewComponent, selectors: [["CollectionView"]], contentQueries: function CollectionViewComponent_ContentQueries(rf, ctx, dirIndex) { if (rf & 1) { i0.ɵɵcontentQuery(dirIndex, TemplateRef, 7, TemplateRef); } if (rf & 2) { let _t; i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.itemTemplateQuery = _t.first); } }, viewQuery: function CollectionViewComponent_Query(rf, ctx) { if (rf & 1) { i0.ɵɵviewQuery(_c0, 7, ViewContainerRef); } if (rf & 2) { let _t; i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.loader = _t.first); } }, hostBindings: function CollectionViewComponent_HostBindings(rf, ctx) { if (rf & 1) { i0.ɵɵlistener("itemLoading", function CollectionViewComponent_itemLoading_HostBindingHandler($event) { return ctx.onItemLoading($event); })("itemRecycling", function CollectionViewComponent_itemRecycling_HostBindingHandler($event) { return ctx.onItemRecyclingInternal($event); })("itemDisposing", function CollectionViewComponent_itemDisposing_HostBindingHandler($event) { return ctx.onItemDisposingInternal($event); }); } }, inputs: { autoReuseViews: "autoReuseViews", itemTemplate: "itemTemplate", items: "items" }, outputs: { setupItemView: "setupItemView" }, decls: 3, vars: 0, consts: [["loader", ""]], template: function CollectionViewComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelementStart(0, "DetachedContainer"); i0.ɵɵelement(1, "Placeholder", null, 0); i0.ɵɵelementEnd(); } }, encapsulation: 2, changeDetection: 0 }); } (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(CollectionViewComponent, [{ type: Component, args: [{ selector: 'CollectionView', template: ` <DetachedContainer> <Placeholder #loader></Placeholder> </DetachedContainer> `, changeDetection: ChangeDetectionStrategy.OnPush, schemas: [NO_ERRORS_SCHEMA] }] }], () => [{ type: i0.ElementRef, decorators: [{ type: Inject, args: [ElementRef] }] }, { type: i0.IterableDiffers, decorators: [{ type: Inject, args: [IterableDiffers] }] }, { type: i1.NativeScriptRendererFactory, decorators: [{ type: Inject, args: [NativeScriptRendererFactory] }] }, { type: i0.NgZone, decorators: [{ type: Inject, args: [NgZone] }] }], { loader: [{ type: ViewChild, args: ['loader', { read: ViewContainerRef, static: true }] }], setupItemView: [{ type: Output }], itemTemplateQuery: [{ type: ContentChild, args: [TemplateRef, { read: TemplateRef, static: true }] }], autoReuseViews: [{ type: Input }], itemTemplate: [{ type: Input }], items: [{ type: Input }], onItemLoading: [{ type: HostListener, args: ['itemLoading', ['$event']] }], onItemRecyclingInternal: [{ type: HostListener, args: ['itemRecycling', ['$event']] }], onItemDisposingInternal: [{ type: HostListener, args: ['itemDisposing', ['$event']] }] }); })(); (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(CollectionViewComponent, { className: "CollectionViewComponent", filePath: "collectionview-comp.ts", lineNumber: 61 }); })(); function getItemViewRoot(viewRef, rootLocator = extractSingleViewRecursive) { const rootView = rootLocator(viewRef.rootNodes, 0); return rootView; } class TemplateKeyDirective { templateRef; collectionView; _templateKey; _rowHeight; constructor(templateRef, collectionView) { this.templateRef = templateRef; this.collectionView = collectionView; } set cvTemplateKey(value) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'cvTemplateKey: ' + value); } this._templateKey = value; if (this.collectionView && this.templateRef) { this.collectionView.registerTemplate(value.toLowerCase(), this.templateRef); if (this._rowHeight !== undefined) { this.collectionView.nativeElement?.setTemplateRowHeight(value, this._rowHeight); } } } set cvRowHeight(value) { this._rowHeight = value; const key = this._templateKey || 'default'; this.collectionView?.nativeElement?.setTemplateRowHeight(key, value); } static ɵfac = function TemplateKeyDirective_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || TemplateKeyDirective)(i0.ɵɵdirectiveInject(i0.TemplateRef), i0.ɵɵdirectiveInject(CollectionViewComponent, 1)); }; static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({ type: TemplateKeyDirective, selectors: [["", "cvTemplateKey", ""], ["", "cvRowHeight", ""]], inputs: { cvTemplateKey: "cvTemplateKey", cvRowHeight: "cvRowHeight" } }); } (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(TemplateKeyDirective, [{ type: Directive, args: [{ selector: '[cvTemplateKey],[cvRowHeight]' }] }], () => [{ type: i0.TemplateRef }, { type: CollectionViewComponent, decorators: [{ type: Host }] }], { cvTemplateKey: [{ type: Input }], cvRowHeight: [{ type: Input }] }); })(); // External class CollectionViewModule { static ɵfac = function CollectionViewModule_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || CollectionViewModule)(); }; static ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: CollectionViewModule }); static ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({}); } (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(CollectionViewModule, [{ type: NgModule, args: [{ imports: [CollectionViewComponent, TemplateKeyDirective], exports: [CollectionViewComponent, TemplateKeyDirective], schemas: [NO_ERRORS_SCHEMA] }] }], null, null); })(); (function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(CollectionViewModule, { imports: [CollectionViewComponent, TemplateKeyDirective], exports: [CollectionViewComponent, TemplateKeyDirective] }); })(); /** * Generated bundle index. Do not edit. */ export { CollectionViewComponent, CollectionViewModule, TemplateKeyDirective }; //# sourceMappingURL=nativescript-community-ui-collectionview-angular.mjs.map