├── src ├── assets │ └── .gitkeep ├── favicon.ico ├── styles.css ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── tsconfig.app.json ├── app │ ├── app.component.css │ ├── app.component.ts │ ├── app.module.ts │ ├── app.component.html │ └── app.component.spec.ts ├── tsconfig.spec.json ├── index.html ├── tslint.json ├── browserslist ├── main.ts ├── test.ts ├── karma.conf.js └── polyfills.ts ├── public_api.ts ├── .github └── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md ├── ng-package.json ├── projects └── ng-vfor-lib │ ├── ng-package.json │ ├── src │ ├── lib │ │ ├── core │ │ │ ├── components │ │ │ │ └── ng-guditems-control │ │ │ │ │ ├── ng-guditems-control.component.css │ │ │ │ │ ├── ng-guditems-control.component.html │ │ │ │ │ ├── ng-guditems-control.component.spec.ts │ │ │ │ │ └── ng-guditems-control.component.ts │ │ │ └── directives │ │ │ │ ├── ng-vFor-container.directive.ts │ │ │ │ └── ng-vFor.directive.ts │ │ └── ng-gudcore.module.ts │ ├── public_api.ts │ └── test.ts │ ├── package.json │ ├── tsconfig.spec.json │ ├── tslint.json │ ├── tsconfig.lib.json │ └── karma.conf.js ├── e2e ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.e2e.json └── protractor.conf.js ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── LICENSE ├── package.json ├── tslint.json ├── angular.json └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public_api.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from 'src/lib/core/modules/ng-gudcore.module'; 3 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anagram4wander/ng-vfor-lib/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } -------------------------------------------------------------------------------- /projects/ng-vfor-lib/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ng-vfor-lib", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "src/test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .data-list-container { 2 | height: 100%; 3 | flex:1; 4 | overflow-y: auto; 5 | position: relative; 6 | } 7 | 8 | .data-list-items-container { 9 | width: 100%; 10 | } 11 | 12 | .total-height { 13 | width: 1px; 14 | opacity: 0; 15 | } 16 | -------------------------------------------------------------------------------- /projects/ng-vfor-lib/src/lib/core/components/ng-guditems-control/ng-guditems-control.component.css: -------------------------------------------------------------------------------- 1 | .nggud-data-list-container { 2 | height: 100%; 3 | flex:1; 4 | overflow-y: auto; 5 | position: relative; 6 | } 7 | 8 | .nggud_total-height { 9 | width: 1px; 10 | opacity: 0; 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /projects/ng-vfor-lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-vfor-lib", 3 | "version": "0.0.5", 4 | "license": "MIT", 5 | "description": "Virtual ngVFor structural directive and container. PURE Virtual !", 6 | "peerDependencies": { 7 | "@angular/common": "^6.1.0", 8 | "@angular/core": "^6.1.0" 9 | } 10 | } -------------------------------------------------------------------------------- /projects/ng-vfor-lib/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ng-vfor-lib 3 | */ 4 | 5 | export * from './lib/ng-gudcore.module'; 6 | export * from './lib/core/components/ng-guditems-control/ng-guditems-control.component'; 7 | export * from './lib/core/directives/ng-vFor-container.directive'; 8 | export * from './lib/core/directives/ng-vFor.directive'; 9 | -------------------------------------------------------------------------------- /projects/ng-vfor-lib/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgGUD 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to ngGUD!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /projects/ng-vfor-lib/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | 14 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'ngGUD'; 10 | 11 | testData = new Array(); 12 | 13 | constructor() { 14 | for (let loop = 0; loop < 100000; loop++) { 15 | this.testData.push('Testing ' + loop); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { NgGUDCoreModule } from '../../projects/ng-vfor-lib/src/public_api'; 6 | 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | imports: [ 13 | BrowserModule, 14 | NgGUDCoreModule 15 | ], 16 | providers: [], 17 | bootstrap: [AppComponent] 18 | }) 19 | export class AppModule { } 20 | -------------------------------------------------------------------------------- /projects/ng-vfor-lib/src/lib/core/components/ng-guditems-control/ng-guditems-control.component.html: -------------------------------------------------------------------------------- 1 |
9 |
10 |
11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | Welcome to {{ title }}! 5 |

6 |
7 | 12 | 13 |
14 | 15 |
16 | {{row}} 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "dom" 19 | ], 20 | "paths": { 21 | "ngVFor-lib": [ 22 | "dist/ng-vfor-lib" 23 | ], 24 | "ngVFor-lib/*": [ 25 | "dist/ng-vfor-lib/*" 26 | ] 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, for easier debugging, you can ignore zone related error 11 | * stack frames such as `zone.run`/`zoneDelegate.invokeTask` by importing the 12 | * below file. Don't forget to comment it out in production mode 13 | * because it will have a performance impact when errors are thrown 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /projects/ng-vfor-lib/src/lib/ng-gudcore.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NgGUDItemsControlComponent } from './core/components/ng-guditems-control/ng-guditems-control.component'; 4 | import { NgVForDirective } from './core/directives/ng-vFor.directive'; 5 | import { NgVForContainerDirective } from './core/directives/ng-vFor-container.directive'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule 10 | ], 11 | declarations: [ 12 | NgGUDItemsControlComponent, 13 | NgVForDirective, 14 | NgVForContainerDirective 15 | ], 16 | providers: [ 17 | ], 18 | exports: [ 19 | NgGUDItemsControlComponent, 20 | NgVForDirective, 21 | NgVForContainerDirective 22 | ] 23 | }) 24 | export class NgGUDCoreModule { } 25 | -------------------------------------------------------------------------------- /projects/ng-vfor-lib/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js/dist/zone'; 5 | import 'zone.js/dist/zone-testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /projects/ng-vfor-lib/src/lib/core/components/ng-guditems-control/ng-guditems-control.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NgGUDItemsControlComponent } from './ng-guditems-control.component'; 4 | 5 | describe('NgGUDItemsControlComponent', () => { 6 | let component: NgGUDItemsControlComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NgGUDItemsControlComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NgGUDItemsControlComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should be created', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ng-vfor-lib 5 | /dist/ng-vfor-lib/esm5 6 | /dist/ng-vfor-lib/esm2015 7 | /dist/ng-vfor-lib/fesm5 8 | /dist/ng-vfor-lib/fesm2015 9 | /dist/ng-vfor-lib/lib 10 | /dist/ng-vfor-lib/ng* 11 | /dist/ng-vfor-lib/package.json 12 | /dist/ng-vfor-lib/public_api.d.ts 13 | 14 | /tmp 15 | /out-tsc 16 | 17 | # dependencies 18 | /node_modules 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | # misc 37 | /.sass-cache 38 | /connect.lock 39 | /coverage 40 | /libpeerconnection.log 41 | npm-debug.log 42 | yarn-error.log 43 | testem.log 44 | /typings 45 | 46 | # System Files 47 | .DS_Store 48 | Thumbs.db 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /projects/ng-vfor-lib/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2015" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": false, 25 | "strictInjectionParameters": true, 26 | "flatModuleId": "AUTOGENERATED", 27 | "flatModuleOutFile": "AUTOGENERATED", 28 | "enableResourceInlining": true 29 | }, 30 | "exclude": [ 31 | "src/test.ts", 32 | "**/*.spec.ts" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /projects/ng-vfor-lib/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'ngGUD'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('ngGUD'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to ngGUD!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 anagram4wander 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /projects/ng-vfor-lib/src/lib/core/components/ng-guditems-control/ng-guditems-control.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, Input, AfterContentInit } from '@angular/core'; 2 | import { NgVForContainerDirective } from '../../directives/ng-vFor-container.directive'; 3 | 4 | @Component({ 5 | // tslint:disable-next-line:component-selector 6 | selector: 'nggud-items-control', 7 | templateUrl: './ng-guditems-control.component.html', 8 | styleUrls: ['./ng-guditems-control.component.css'] 9 | }) 10 | export class NgGUDItemsControlComponent implements OnInit, AfterContentInit { 11 | private _target; 12 | 13 | @ViewChild(NgVForContainerDirective) _container: NgVForContainerDirective; 14 | 15 | @Input() scrollTop: Number = 0; 16 | @Input() scrollbarWidth: number; 17 | @Input() scrollbarHeight: number; 18 | @Input() customSize: Function = null; 19 | 20 | constructor() { } 21 | 22 | ngOnInit() { 23 | } 24 | 25 | 26 | public attach(target) { 27 | this._target = target; 28 | } 29 | 30 | ngAfterContentInit() { 31 | if (this._container) { 32 | console.log('got container'); 33 | this._container.attachUpdate(this._target); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-gud", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "build_lib": "ng build ngVFor-lib", 12 | "npm_pack": "cd dist/ng-vfor-lib && npm pack", 13 | "package": "npm run build_lib && npm run npm_pack" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "^6.1.0", 18 | "@angular/common": "^6.1.0", 19 | "@angular/compiler": "^6.1.0", 20 | "@angular/core": "^6.1.0", 21 | "@angular/forms": "^6.1.0", 22 | "@angular/http": "^6.1.0", 23 | "@angular/platform-browser": "^6.1.0", 24 | "@angular/platform-browser-dynamic": "^6.1.0", 25 | "@angular/router": "^6.1.0", 26 | "core-js": "^2.5.4", 27 | "rxjs": "^6.0.0", 28 | "zone.js": "~0.8.26" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "~0.8.0-beta.0", 32 | "@angular-devkit/build-ng-packagr": "~0.8.0-beta.0", 33 | "@angular/cli": "~6.2.0-beta.2", 34 | "@angular/compiler-cli": "^6.1.0", 35 | "@angular/language-service": "^6.1.0", 36 | "@types/jasmine": "~2.8.8", 37 | "@types/jasminewd2": "~2.0.3", 38 | "@types/node": "~8.9.4", 39 | "codelyzer": "~4.3.0", 40 | "jasmine-core": "~2.99.1", 41 | "jasmine-spec-reporter": "~4.2.1", 42 | "karma": "~1.7.1", 43 | "karma-chrome-launcher": "~2.2.0", 44 | "karma-coverage-istanbul-reporter": "~2.0.1", 45 | "karma-jasmine": "~1.1.2", 46 | "karma-jasmine-html-reporter": "^0.2.2", 47 | "ng-packagr": "^4.1.0", 48 | "protractor": "~5.4.0", 49 | "ts-node": "~7.0.0", 50 | "tsickle": ">=0.29.0", 51 | "tslib": "^1.9.0", 52 | "tslint": "~5.11.0", 53 | "typescript": "~2.9.2" 54 | }, 55 | "peerDependencies": { 56 | "@angular/compiler": "^6.0.0", 57 | "@angular/compiler-cli": "^6.0.0", 58 | "tsickle": ">=0.27.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngGUD": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "targets": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/ngGUD", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "src/environments/environment.ts", 35 | "with": "src/environments/environment.prod.ts" 36 | } 37 | ], 38 | "optimization": true, 39 | "outputHashing": "all", 40 | "sourceMap": false, 41 | "extractCss": true, 42 | "namedChunks": false, 43 | "aot": true, 44 | "extractLicenses": true, 45 | "vendorChunk": false, 46 | "buildOptimizer": true 47 | } 48 | } 49 | }, 50 | "serve": { 51 | "builder": "@angular-devkit/build-angular:dev-server", 52 | "options": { 53 | "browserTarget": "ngGUD:build" 54 | }, 55 | "configurations": { 56 | "production": { 57 | "browserTarget": "ngGUD:build:production" 58 | } 59 | } 60 | }, 61 | "extract-i18n": { 62 | "builder": "@angular-devkit/build-angular:extract-i18n", 63 | "options": { 64 | "browserTarget": "ngGUD:build" 65 | } 66 | }, 67 | "test": { 68 | "builder": "@angular-devkit/build-angular:karma", 69 | "options": { 70 | "main": "src/test.ts", 71 | "polyfills": "src/polyfills.ts", 72 | "tsConfig": "src/tsconfig.spec.json", 73 | "karmaConfig": "src/karma.conf.js", 74 | "styles": [ 75 | "src/styles.css" 76 | ], 77 | "scripts": [], 78 | "assets": [ 79 | "src/favicon.ico", 80 | "src/assets" 81 | ] 82 | } 83 | }, 84 | "lint": { 85 | "builder": "@angular-devkit/build-angular:tslint", 86 | "options": { 87 | "tsConfig": [ 88 | "src/tsconfig.app.json", 89 | "src/tsconfig.spec.json" 90 | ], 91 | "exclude": [ 92 | "**/node_modules/**" 93 | ] 94 | } 95 | } 96 | } 97 | }, 98 | "ngGUD-e2e": { 99 | "root": "e2e/", 100 | "projectType": "application", 101 | "targets": { 102 | "e2e": { 103 | "builder": "@angular-devkit/build-angular:protractor", 104 | "options": { 105 | "protractorConfig": "e2e/protractor.conf.js", 106 | "devServerTarget": "ngGUD:serve" 107 | }, 108 | "configurations": { 109 | "production": { 110 | "devServerTarget": "ngGUD:serve:production" 111 | } 112 | } 113 | }, 114 | "lint": { 115 | "builder": "@angular-devkit/build-angular:tslint", 116 | "options": { 117 | "tsConfig": "e2e/tsconfig.e2e.json", 118 | "exclude": [ 119 | "**/node_modules/**" 120 | ] 121 | } 122 | } 123 | } 124 | }, 125 | "ngVFor-lib": { 126 | "root": "projects/ng-vfor-lib", 127 | "sourceRoot": "projects/ng-vfor-lib/src", 128 | "projectType": "library", 129 | "prefix": "lib", 130 | "targets": { 131 | "build": { 132 | "builder": "@angular-devkit/build-ng-packagr:build", 133 | "options": { 134 | "tsConfig": "projects/ng-vfor-lib/tsconfig.lib.json", 135 | "project": "projects/ng-vfor-lib/ng-package.json" 136 | } 137 | }, 138 | "test": { 139 | "builder": "@angular-devkit/build-angular:karma", 140 | "options": { 141 | "main": "projects/ng-vfor-lib/src/test.ts", 142 | "tsConfig": "projects/ng-vfor-lib/tsconfig.spec.json", 143 | "karmaConfig": "projects/ng-vfor-lib/karma.conf.js" 144 | } 145 | }, 146 | "lint": { 147 | "builder": "@angular-devkit/build-angular:tslint", 148 | "options": { 149 | "tsConfig": [ 150 | "projects/ng-vfor-lib/tsconfig.lib.json", 151 | "projects/ng-vfor-lib/tsconfig.spec.json" 152 | ], 153 | "exclude": [ 154 | "**/node_modules/**" 155 | ] 156 | } 157 | } 158 | } 159 | } 160 | }, 161 | "defaultProject": "ngGUD" 162 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Virtualization of ngFor — Welcome to the pure ngFor replacement — ngVFor ! 2 | 3 | We love the Angular framework at [Alpha Chi Technology](http://www.alphachitech.com). It’s a fantastic 4 | framework that really does expose most of what we need. We have been using it 5 | since version 1 to produce some of the most complex, deep systems around. BIG 6 | systems. But — this is a big caveat — creating these large applications with 7 | lots of large data comes with a number of technical challenges. 8 | 9 | Let’s look at the challenge of showing a list that contains hundreds of 10 | thousands of items. One of the most common ways to approach it is to use an 11 | infinite scroll directive, loading the data on demand as you near the end. That 12 | works well in simple situations but has its obvious limitations: repeated 13 | scrolling down to find something, complex sorts that preclude this approach. You 14 | may find your nice application come crawling to a stop, maybe even becoming 15 | totally unresponsive. 16 | 17 | The reason for this is the ngFor directive. This directive exists pretty well 18 | everywhere. Even in some sort of scrolling container, it loads every entry into 19 | the DOM. So, if you have hundreds of thousands of items, you are going to have 20 | hundreds of thousands of DOM objects for every entry. Argh — slow down ahead. 21 | 22 | The correct way to solve it is to use a virtualization container, so only the 23 | items actually showing on the screen (the viewport) are rendered into the DOM. 24 | This means no matter how much data you throw at your list, the only effect is 25 | that your scroll handles get smaller. 26 | 27 | There are a couple of virtualization containers for Angular out there. We know, 28 | my team has tried or used most of them at one time or another ([Rinto 29 | Joses](https://medium.com/@rintoj/building-virtual-scroll-for-angular-2-7679ca95014e)’ 30 | is the best one — our container measure used this as its starting point, and it 31 | contains a good explanation of how a viewport works. It’s a great read to 32 | understand basic virtualization). But, even the new virtualization container in 33 | Angular CDK experimental 6.2 has a number of critical limitations. Namely: 34 | 35 | · **They don’t support changes nicely.** Either they re-render the whole 36 | viewport or they don’t support changes to the collection at all. Basically, 37 | every one we have found creates a buffer array by slicing out the items in 38 | viewport, and then have a ngFor on that buffer. That means new data occurring 39 | inside the viewport doesn’t get updated. 40 | 41 | · **They don’t perform well with variable sized elements,** if they support it 42 | all (and we are not talking about just the elements that have been already 43 | rendered). 44 | 45 | · **They all need some sort of container to work.** You can’t just do a global 46 | replace on ngFor then add the containers as you need them. 47 | 48 | ***** 49 | 50 | Our team created a new directive that doesn’t have any of those limitations. 51 | Welcome to ngVFor! 52 | 53 | You can find the npm [here](https://www.npmjs.com/package/ngvforlib), and the 54 | github with the source, [here](https://github.com/anagram4wander/ng-vfor-lib). 55 | 56 | Let’s start with a simple example using the existing ngFor directive, then 57 | expand it to use the new ngVFor. Once we have done that, look towards the end of 58 | the article for an explanation of how it all works. 59 | 60 | We will create an example that highlights the problem we want to solve: a 61 | contained area with scrollbars and an ngFor over 100,000 items of data. 62 | 63 | Let’s look at the sample, in our case we did ng new projectName and then added 64 | the data to the app.component.ts, the container style to the app.component.css, 65 | and the container and ngFor into the app.component.html 66 | 67 | So, they look like: 68 | 69 | **app.component.ts** 70 | 71 | ``` 72 | import { Component } from '@angular/core'; 73 | 74 | @Component({ 75 | selector: 'app-root', 76 | templateUrl: './app.component.html', 77 | styleUrls: ['./app.component.css'] 78 | }) 79 | export class AppComponent { 80 | title = 'lib-tester'; 81 | 82 | testData = new Array(); 83 | 84 | constructor() { 85 | for (let loop = 0; loop < 100000; loop++) { 86 | this.testData.push('Testing ' + loop); 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | **app.component.css** 93 | 94 | 95 | 96 | ``` 97 | .data-list-container { 98 | height: 100%; 99 | flex:1; 100 | overflow-y: auto; 101 | position: relative; 102 | } 103 | 104 | ``` 105 | **app.component.html** 106 | 107 | ``` 108 |
109 |

110 | Welcome to {{ title }}! 111 |

112 |
113 |
115 |
117 | {{row}} 118 |
119 |
120 | ``` 121 | 122 | Now let’s run it. Use the scroll bar to (try to) scroll down to the 300th item. 123 | As you can see, the performance is horrid. I nearly gave up waiting for it to 124 | even render at all in Microsoft Edge, although once it does eventually load, the 125 | scroll performance is better than Chrome. 126 | 127 | Here is the performance profile on Chrome. Basically, it locks up the process 128 | for about five seconds per jump on the scrollbar. Yup, that’s a framerate of 1/5 129 | FPS — one fifth of a frame per second. 130 | 131 | ![](https://cdn-images-1.medium.com/max/1000/1*wDN8Ck7tWGjsD22zbFNjhQ.jpeg) 132 | Figure 1 - Using just ngFor Chrone output with 100,000 items 133 | 134 | ***** 135 | 136 | ### Adding in ngVFor 137 | 138 | Time for some improvements. First we need the npm package with the virtual 139 | ngVFor implementation. 140 | 141 | ``` 142 | npm install ngvforlib 143 | ``` 144 | 145 | And then import and add to the imports section the `NgGUDCoreModule` into 146 | **app.module.ts** something like this 147 | 148 | ``` 149 | import { BrowserModule } from '@angular/platform-browser'; 150 | import { NgModule } from '@angular/core'; 151 | import { AppComponent } from './app.component'; 152 | import { NgGUDCoreModule } from 'ng-vfor-lib'; 153 | 154 | @NgModule({ 155 | declarations: [ 156 | AppComponent 157 | ], 158 | imports: [ 159 | BrowserModule, 160 | NgGUDCoreModule 161 | ], 162 | providers: [], 163 | bootstrap: [AppComponent] 164 | }) 165 | export class AppModule { } 166 | ``` 167 | 168 | > Note: Why the name ? The non github version of NgGUDCoreModule contains a lot of 169 | > other unique development that we didn’t include in the free-to-use version : 170 | virtual trees and paginated data sources that support client-side edits just to 171 | name a couple, plus a host of super-fast enterprise controls to produce large 172 | scale systems. Ping us if you need them or want to use our services. 173 | 174 | > NgGUD -> Angular Grown Up Data. 175 | 176 | Now its time to introduce the `ngVFor` directive. It should be noted that 177 | `ngVFor `is 100% compatible with `ngFor`, so we can simply do a global replace 178 | of ‘*ngFor’ with ‘*ngVFor’ in all the HTML if we want to. 179 | 180 | In our case, the **app.component.html** ends up as: 181 | 182 | ``` 183 |
184 |

185 | Welcome to {{ title }}! 186 |

187 |
188 |
190 |
193 | {{row}} 194 |
195 |
196 | 197 | ``` 198 | 199 | Now let’s debug it again. When you run it this time you will notice little 200 | difference, maybe the scroll jumps move a tad faster, maybe 1/3 FPS, but the 201 | scroll into the 300’s is still terrible. 202 | 203 | > Note: If you get a lot of adds/deletes in your data source, the ngVFor will 204 | > actually perform considerably faster as it reuses the templates rather than 205 | recreating them each time. 206 | 207 | The Chrome performance stats show this: 208 | 209 | ![](https://cdn-images-1.medium.com/max/1000/1*ArKy1Ogo-3Bphrm9suHw1g.jpeg) 210 | Figure 2 - Just changing the ngFor to an ngVFor 211 | 212 | 213 | ***** 214 | 215 | ### Adding a Virtualization container 216 | 217 | Now let’s add a virtualization scroll container. This will speed things up a 218 | lot. In our case the container is a component `nggud-items-control` this manages 219 | the scroll and measuring of the viewport — which is then supplied down to the 220 | `ngVFor `directive to manage the visible viewport. 221 | 222 | In this case its as simple as bracing the `ngVFor` in the app.component.html 223 | 224 | ``` 225 |
226 |

227 | Welcome to {{ title }}! 228 |

229 |
230 | 231 |
232 | 233 |
235 | {{row}} 236 |
237 |
238 |
239 | 240 | ``` 241 | 242 | Run it again, you will notice a couple of things: 1) It loads way faster, 2) The 243 | scrolling is way faster. The performance stats confirm this: the rendering has 244 | gone from 6700ms and 1/5 frame per second to 75ms and about 50 frames per second 245 | — basically a **90 times** speed improvement: 246 | 247 | ![](https://cdn-images-1.medium.com/max/1000/1*sD9deoWKmQzR4saC5IUJAA.jpeg) 248 | Figure 3 - ngVFor and container running on Chrome 249 | 250 | The one and only limitation of the `nggud-items-control `is that the container 251 | must be expanded to it’s target size, rather than be allowed to either expand 252 | automatically, or just have a `maxheight` for example. 253 | 254 | ***** 255 | 256 | ### nggud-items-control `@Inputs` 257 | 258 | The container control has a couple of useful inputs that allow configuration. 259 | 260 | * `scrollTop`: This is the current top position of the viewport. If you want to 261 | support navigation away from a view, and when you return to it, the scroll 262 | position is retained, then you will want to bind this. 263 | * `scrollbarWidth`: Override when you have a overridden scrollbar visual 264 | implementation. 265 | * `scrollbarHeight`: Override when you have a overridden scrollbar visual 266 | implementation. 267 | * `customSize`: Function that allows you to deal with custom size elements. This 268 | is going to be covered in the next part of the article. 269 | 270 | ***** 271 | 272 | ### **Ok, that’s it on how to use it, now lets move on to how it works.** 273 | 274 | Coming soon — part 2 275 | -------------------------------------------------------------------------------- /projects/ng-vfor-lib/src/lib/core/directives/ng-vFor-container.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | ElementRef, 4 | Input, 5 | OnInit, 6 | HostListener, 7 | ContentChild, 8 | AfterContentInit, 9 | OnDestroy, 10 | AfterViewInit 11 | } from '@angular/core'; 12 | import { Subject, Subscription, of } from 'rxjs'; 13 | import { switchMap} from 'rxjs/operators'; 14 | 15 | @Directive({ 16 | // tslint:disable-next-line:directive-selector 17 | selector: '[ngVForContainer]' 18 | }) 19 | export class NgVForContainerDirective implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { 20 | 21 | @Input() scrollbarWidth: number; 22 | @Input() scrollbarHeight: number; 23 | @Input() customSize: Function = null; 24 | @ContentChild('totalHeight') totalHeight: ElementRef; 25 | 26 | scroll$: Subject = new Subject(); 27 | scrollHeight: number; 28 | 29 | private previousStart: number; 30 | private previousEnd: number; 31 | private startupLoop = true; 32 | private counter = 5; 33 | private saved_child_height = 0; 34 | private saved_child_width = 0; 35 | private dimensions: any; 36 | private scrollSubscription: Subscription = null; 37 | private vrItems = null; 38 | 39 | @HostListener('scroll') 40 | onScroll(e: Event) { 41 | this.scroll$.next(); 42 | } 43 | 44 | constructor(private elRef: ElementRef /*, private _channelService: NgGUDVForChannelService */) { 45 | } 46 | 47 | ngOnInit() { 48 | this.scroll$.pipe(switchMap(() => { 49 | this.refresh(); 50 | return of(); 51 | })).subscribe(() => { }); 52 | this.scrollbarWidth = 0; 53 | this.scrollbarHeight = 0; 54 | 55 | } 56 | 57 | ngOnDestroy() { 58 | this.vrItems = null; 59 | this.scroll$.complete(); 60 | } 61 | 62 | directAttach(target) { 63 | console.log('direct attach'); 64 | this.vrItems = target; 65 | } 66 | 67 | ngAfterContentInit() { 68 | console.log('now attaching update'); 69 | if (this.vrItems !== null && this.vrItems !== undefined) { 70 | this.attachUpdate(this.vrItems); 71 | } 72 | } 73 | 74 | ngAfterViewInit() { 75 | } 76 | 77 | @Input() 78 | public get scrollTop(): number { 79 | let ret = 0; 80 | const el = this.elRef.nativeElement; 81 | 82 | if (el) { 83 | ret = el.scrollTop; 84 | } 85 | 86 | return ret; 87 | } 88 | public set scrollTop(newScrollTop: number) { 89 | const el = this.elRef.nativeElement; 90 | 91 | if (el) { 92 | el.scrollTop = newScrollTop; 93 | this.refresh(); 94 | } 95 | } 96 | 97 | public attachUpdate(element) { 98 | if (this.vrItems === null || this.vrItems === undefined) { 99 | this.vrItems = element; 100 | } 101 | // Wire in the update event 102 | element.update.subscribe((u) => { 103 | this.refresh(); 104 | }); 105 | if (element.length > 0) { 106 | this.refresh(); 107 | } 108 | 109 | } 110 | 111 | private refresh() { 112 | this.calculateDimensions(); 113 | this.calculateItems(false, this.counter); 114 | } 115 | 116 | private countItemsPerRow() { 117 | // let offsetTop; 118 | // let itemsPerRow; 119 | // // let children = this.contentElementRef.nativeElement.children; 120 | // let children = this.elRef.nativeElement.children; 121 | // for (itemsPerRow = 0; itemsPerRow < children.length; itemsPerRow++) { 122 | // if (offsetTop !== undefined && offsetTop !== children[itemsPerRow].offsetTop) { break; } 123 | // offsetTop = children[itemsPerRow].offsetTop; 124 | // } 125 | // return itemsPerRow; 126 | return 1; 127 | } 128 | 129 | protected getCustomSizeAdjustments(startIndex: number, extentLength: number, totalLength: number) { 130 | let adjustments = { adjustmentBefore: 0, beforeCount: 0, adjustmentStart: 0, startCount: 0, adjustmentAfter: 0, afterCount: 0 }; 131 | 132 | if (this.customSize !== null && this.customSize !== undefined) { 133 | adjustments = this.customSize(startIndex, extentLength, totalLength); 134 | } 135 | 136 | return adjustments; 137 | } 138 | 139 | private calculateDimensions() { 140 | if (!this.vrItems || (this.vrItems && !this.vrItems.length)) { return; } 141 | const container = this.vrItems.content; 142 | const itemCount = this.vrItems.length; 143 | const viewWidth = this.elRef.nativeElement.clientWidth - this.scrollbarWidth; 144 | const viewHeight = this.elRef.nativeElement.clientHeight - this.scrollbarHeight; 145 | 146 | const contentDimensions = container.children[0] ? 147 | { 148 | width: container.children[0].clientWidth, 149 | height: container.children[0].clientHeight 150 | } : 151 | { 152 | width: viewWidth, 153 | height: viewHeight 154 | }; 155 | let index = 0; 156 | if (this.previousStart >= 0) { 157 | index = this.previousStart; 158 | } 159 | let childWidth = contentDimensions.width; 160 | let childHeight = contentDimensions.height; 161 | 162 | if (this.saved_child_height > 0) { 163 | childHeight = this.saved_child_height; 164 | childWidth = this.saved_child_width; 165 | } else { 166 | if (container.children[0]) { 167 | this.saved_child_height = childHeight; 168 | this.saved_child_width = childWidth; 169 | } 170 | } 171 | 172 | const itemsPerRow = Math.max(1, this.countItemsPerRow()); 173 | // let itemsPerRowByCalc = Math.max(1, Math.floor(viewWidth / childWidth)); 174 | const itemsPerCol = Math.max(1, Math.floor(viewHeight / childHeight)); 175 | // let scrollTop = Math.max(0, this.elRef.nativeElement.scrollTop); 176 | // if (itemsPerCol === 1 && Math.floor(scrollTop / this.scrollHeight * itemCount) + itemsPerRowByCalc >= itemCount) { 177 | // itemsPerRow = itemsPerRowByCalc; 178 | // } 179 | 180 | this.dimensions = { 181 | viewWidth: viewWidth, 182 | viewHeight: viewHeight, 183 | childWidth: childWidth, 184 | childHeight: childHeight, 185 | itemsPerRow: itemsPerRow, 186 | itemsPerCol: itemsPerCol, 187 | // itemsPerRowByCalc: itemsPerRowByCalc 188 | }; 189 | } 190 | 191 | private calculateItems(newItems: boolean, retry: number) { 192 | if (!this.dimensions) { 193 | return; 194 | } else if (!this.dimensions.childHeight && retry > 0) { // first item didn't render in time 195 | setTimeout(() => { 196 | console.log(`Retrying rendering items, retries remaining: ${retry - 1}`); 197 | }, 200); 198 | } else { 199 | const visited = new Object(); 200 | const el = this.elRef.nativeElement; 201 | const itemsLength = this.vrItems.length; 202 | const d = this.dimensions; 203 | let needsAdjustment = false; 204 | 205 | // Phase 1 - Lets set the scrollHeight 206 | // Get the total scope of the adjustments to the default height.. 207 | let adjustments = this.getCustomSizeAdjustments(0, itemsLength - 1, itemsLength); 208 | if (adjustments.adjustmentBefore !== 0 || adjustments.adjustmentAfter !== 0) { 209 | needsAdjustment = true; 210 | } 211 | this.scrollHeight = (d.childHeight * itemsLength / d.itemsPerRow) + adjustments.adjustmentBefore + adjustments.adjustmentAfter; 212 | this.totalHeight.nativeElement.style.height = `${this.scrollHeight}px`; 213 | if (this.elRef.nativeElement.scrollTop > this.scrollHeight) { 214 | this.elRef.nativeElement.scrollTop = this.scrollHeight; 215 | } 216 | 217 | // Phase 2 - now lets work out the start and end indexes 218 | let indexSeekDone = false; 219 | let scrollTop = el.scrollTop; 220 | let end = -1; 221 | let start = -1; 222 | adjustments = { adjustmentBefore: 0, beforeCount: 0, adjustmentStart: 0, startCount: 0, adjustmentAfter: 0, afterCount: 0 }; 223 | 224 | while (!indexSeekDone) { 225 | // const indexByScrollTop = Math.max(0, scrollTop) / this.scrollHeight * itemsLength / d.itemsPerRow; 226 | const indexByScrollTop = scrollTop / d.childHeight; 227 | end = Math.min(itemsLength, Math.ceil(indexByScrollTop) * d.itemsPerRow + d.itemsPerRow * (d.itemsPerCol + 1)); 228 | 229 | let maxStartEnd = end; 230 | const modEnd = end % d.itemsPerRow; 231 | if (modEnd) { 232 | maxStartEnd = end + d.itemsPerRow - modEnd; 233 | } 234 | const maxStart = Math.max(0, maxStartEnd - d.itemsPerCol * d.itemsPerRow - d.itemsPerRow); 235 | start = Math.min(maxStart, Math.floor(indexByScrollTop) * d.itemsPerRow); 236 | 237 | start = !isNaN(start) ? start : -1; 238 | end = !isNaN(end) ? end : -1; 239 | 240 | 241 | if (!needsAdjustment) { 242 | // There are no adjustments required 243 | indexSeekDone = true; 244 | } else { 245 | if (visited['' + start] !== undefined) { 246 | indexSeekDone = true; 247 | } else { 248 | visited['' + start] = start; 249 | } 250 | const newAdjustments = this.getCustomSizeAdjustments(start, end - start, itemsLength); 251 | newAdjustments.adjustmentBefore -= newAdjustments.beforeCount * d.childHeight; 252 | newAdjustments.adjustmentStart -= newAdjustments.startCount * d.childHeight; 253 | newAdjustments.adjustmentAfter -= newAdjustments.afterCount * d.childHeight; 254 | const diff = newAdjustments.adjustmentBefore - adjustments.adjustmentBefore; 255 | 256 | if (newAdjustments.adjustmentBefore === 0 || indexSeekDone) { 257 | indexSeekDone = true; 258 | } else { 259 | // We need to do an adjustment, so adjust the scrollTop and go again.. 260 | scrollTop -= diff; 261 | } 262 | adjustments = newAdjustments; 263 | } 264 | } 265 | 266 | // Phase 3 - start and end adjusted, now do the gets 267 | 268 | if (start !== this.previousStart || end !== this.previousEnd || newItems) { 269 | this.previousStart = start; 270 | this.previousEnd = end; 271 | 272 | // update the scroll position 273 | const topPosition = Math.max(0, d.childHeight * Math.ceil(start / d.itemsPerRow) + adjustments.adjustmentBefore); 274 | 275 | // update the scroll list 276 | setTimeout(() => { 277 | this.vrItems.setTargetViewPort(topPosition, start, end); 278 | }, 20); 279 | } 280 | 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /projects/ng-vfor-lib/src/lib/core/directives/ng-vFor.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, DoCheck, EmbeddedViewRef, Input, IterableChangeRecord, 3 | OnInit, OnChanges, SimpleChanges, IterableChanges, IterableDiffer, IterableDiffers, 4 | TemplateRef, TrackByFunction, ElementRef, ViewContainerRef, isDevMode, EventEmitter, 5 | Output, AfterContentInit, OnDestroy, SkipSelf, Optional 6 | } from '@angular/core'; 7 | import { 8 | NgForOfContext 9 | } from '@angular/common'; 10 | 11 | import { NgGUDItemsControlComponent} from '../components/ng-guditems-control/ng-guditems-control.component'; 12 | 13 | export type NgGUDIterable = Array | Iterable; 14 | 15 | class ViewPort { 16 | 17 | constructor(topPosition = 0, start = 0, end = 0) { 18 | this.topPosition = topPosition; 19 | this.start = start; 20 | this.end = end; 21 | } 22 | 23 | topPosition = 0; 24 | start = 0; 25 | end = 0; 26 | } 27 | 28 | export class NgVForNeedsUpdateArgs { 29 | constructor(private _length) { 30 | } 31 | 32 | public get length(): number { 33 | return this._length; 34 | } 35 | } 36 | 37 | export class NgVForRenderUpdatedArgs extends ViewPort { 38 | 39 | } 40 | 41 | class ViewPortDimensions { 42 | containerHeight = -1; 43 | standardItemHeight = -1; 44 | } 45 | 46 | @Directive({ 47 | // tslint:disable-next-line:directive-selector 48 | selector: '[ngVFor]' 49 | }) 50 | export class NgVForDirective implements OnChanges, OnDestroy, OnInit, DoCheck, AfterContentInit { 51 | 52 | constructor( 53 | private _elRef: ElementRef, 54 | private _viewContainer: ViewContainerRef, 55 | private _template: TemplateRef>, 56 | private _differs: IterableDiffers, 57 | @Optional() @SkipSelf() private _viewPortContainer: NgGUDItemsControlComponent 58 | ) { } 59 | 60 | private parentType: string; 61 | // tslint:disable-next-line:no-inferrable-types 62 | private _topPosition: number = 0; 63 | // tslint:disable-next-line:no-inferrable-types 64 | private _dirty: boolean = true; 65 | private _iterable: NgGUDIterable; 66 | private _differ: IterableDiffer | null = null; 67 | private _viewPortDimensions: ViewPortDimensions = null; 68 | private _lastViewPort: ViewPort = null; 69 | // tslint:disable-next-line:no-inferrable-types 70 | private _length: number = 0; 71 | private _newViewPort: ViewPort = null; 72 | private _cachedViews: EmbeddedViewRef>[] = []; 73 | private _trackByFn !: TrackByFunction; 74 | private _noContainer = false; 75 | 76 | // region Properties 77 | 78 | public content: HTMLElement; 79 | 80 | public get length(): number { 81 | return this._length; 82 | } 83 | 84 | get topPosition(): number { 85 | return this._topPosition; 86 | } 87 | set topPosition(value: number) { 88 | if (this._topPosition !== value) { 89 | this._topPosition = value; 90 | this.setTopPosition(value); 91 | } 92 | } 93 | 94 | // endregion Properties 95 | 96 | // region angular properties 97 | 98 | @Input() 99 | set ngForTemplate(value: TemplateRef>) { 100 | if (value) { 101 | this._template = value; 102 | } 103 | } 104 | 105 | @Input() 106 | set ngVForOf(value: NgGUDIterable) { 107 | this._iterable = value; 108 | this._dirty = true; 109 | } 110 | 111 | @Input() 112 | items: any; 113 | 114 | @Input() 115 | lockPositionOnUpdate = false; 116 | 117 | @Input() 118 | public cachedViewsBufferSize = 0; 119 | 120 | @Input() 121 | set ngForTrackBy(fn: TrackByFunction) { 122 | if (isDevMode() && fn != null && typeof fn !== 'function') { 123 | // TODO(vicb): use a log service once there is a public one available 124 | if (console && console.warn) { 125 | console.warn( 126 | `trackBy must be a function, but received ${JSON.stringify(fn)}. ` + 127 | `See https://angular.io/docs/ts/latest/api/common/index/NgFor-directive.html#!#change-propagation for more information.`); 128 | } 129 | } 130 | this._trackByFn = fn; 131 | } 132 | get ngForTrackBy(): TrackByFunction { return this._trackByFn; } 133 | 134 | @Output() update: EventEmitter = new EventEmitter(); 135 | 136 | @Output() viewPortUpdated: EventEmitter = new EventEmitter(); 137 | 138 | // endregion angular properties 139 | 140 | // region Angular hooks 141 | 142 | ngOnInit() { 143 | this.content = this._elRef.nativeElement.parentElement; 144 | this.parentType = this._elRef.nativeElement.parentElement.localName; 145 | if (this.parentType === 'table') { 146 | this._elRef.nativeElement.parentElement.style.position = 'absolute'; 147 | } else { 148 | this.content.style.position = 'absolute'; 149 | } 150 | if (this._viewPortContainer !== null && this._viewPortContainer !== undefined) { 151 | console.log('got viewport container'); 152 | this._viewPortContainer.attach(this); 153 | } else { 154 | this._noContainer = true; 155 | this.ngDoCheck(); 156 | } 157 | } 158 | 159 | ngAfterContentInit() { 160 | } 161 | 162 | ngOnChanges(changes: SimpleChanges) { 163 | } 164 | 165 | ngOnDestroy() { 166 | for (let loop = 0; loop < this._cachedViews.length; loop++) { 167 | this._cachedViews[loop].destroy(); 168 | } 169 | this._cachedViews = []; 170 | this.update.complete(); 171 | } 172 | 173 | ngDoCheck(): void { 174 | if (this._dirty) { 175 | console.log('in do check'); 176 | this._dirty = false; 177 | if (!this._differ && this._iterable) { 178 | try { 179 | this._differ = this._differs.find(this._iterable).create(this.ngForTrackBy); 180 | } catch (e) { 181 | 182 | } 183 | } 184 | 185 | if (this._differ) { 186 | const changes = this._differ.diff(this._iterable); 187 | if (changes) { this.applyChanges(changes); } 188 | } 189 | } 190 | if (this._newViewPort !== null && this._newViewPort !== undefined) { 191 | if (this._noContainer === false) { 192 | const nvp = this._newViewPort; 193 | this._newViewPort = null; 194 | this.renderViewPort(nvp.topPosition, nvp.start, nvp.end); 195 | } 196 | } 197 | } 198 | 199 | // endregion Angular hooks 200 | 201 | // region changes 202 | 203 | private applyChanges(changes: IterableChanges) { 204 | // if we are at the start of the render pass, we dont know the height of anything, so render out just the first item 205 | let vp: ViewPort = null; 206 | 207 | if (this._viewPortDimensions === null) { 208 | vp = new ViewPort(0, 0, 0); 209 | } else { 210 | vp = this._lastViewPort; 211 | } 212 | this._lastViewPort = vp; 213 | 214 | changes.forEachOperation( 215 | (item: IterableChangeRecord, adjustedPreviousIndex: number, currentIndex: number) => { 216 | if (item.previousIndex == null) { 217 | // insert 218 | this._length++; 219 | if (this._noContainer === true || (currentIndex >= vp.start && currentIndex <= vp.end)) { 220 | let view = this.getNewView(); 221 | if (view) { 222 | this._viewContainer.insert(view, currentIndex - vp.start); 223 | view.context.$implicit = item.item; 224 | } else { 225 | view = this._viewContainer.createEmbeddedView( 226 | this._template, new NgForOfContext(item.item, this._iterable, -1, -1), currentIndex - vp.start); 227 | } 228 | } 229 | } else if (currentIndex == null) { 230 | // remove 231 | this._length--; 232 | if (this._noContainer === true || (adjustedPreviousIndex >= vp.start && adjustedPreviousIndex <= vp.end)) { 233 | this.cacheView(this._viewContainer.detach(adjustedPreviousIndex - vp.start) as EmbeddedViewRef>); 234 | } 235 | } else { 236 | // move 237 | const view = this._viewContainer.get(adjustedPreviousIndex); 238 | if (view !== null && view !== undefined) { 239 | if (this._noContainer === true || (currentIndex >= vp.start && currentIndex <= vp.end && 240 | adjustedPreviousIndex >= vp.start && adjustedPreviousIndex <= vp.end)) { 241 | this._viewContainer.move(view, currentIndex - vp.start); 242 | } else { 243 | if (adjustedPreviousIndex >= vp.start && adjustedPreviousIndex <= vp.end) { 244 | this.cacheView(this._viewContainer.detach(adjustedPreviousIndex - vp.start) as EmbeddedViewRef>); 245 | } 246 | } 247 | } 248 | } 249 | }); 250 | 251 | for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) { 252 | const viewRef = >>this._viewContainer.get(i); 253 | if (viewRef !== null && viewRef !== undefined) { 254 | viewRef.context.index = i + vp.start; 255 | viewRef.context.count = this._length; 256 | viewRef.context.ngForOf = this._iterable; 257 | } 258 | } 259 | 260 | changes.forEachIdentityChange((record: any) => { 261 | if (this._noContainer === true || (record.currentIndex >= vp.start && record.currentIndex <= vp.end)) { 262 | const viewRef = 263 | >>this._viewContainer.get(record.currentIndex - vp.start); 264 | if (viewRef !== null && viewRef !== undefined) { 265 | viewRef.context.$implicit = record.item; 266 | } 267 | } 268 | }); 269 | 270 | // If the length has shortened the viewport then we need to take that into account 271 | if (this.length < vp.end) { 272 | vp.end = this.length; 273 | vp.start = Math.min(vp.start, this.length); 274 | this._lastViewPort = vp; 275 | } 276 | 277 | // emit a somethings changed event 278 | this.update.emit(new NgVForNeedsUpdateArgs(this.length)); 279 | 280 | } 281 | 282 | // endregion changes 283 | 284 | public setTargetViewPort(topPosition: number, start: number, end: number) { 285 | this._newViewPort = new ViewPort(topPosition, start, end); 286 | } 287 | 288 | // region Implementation 289 | 290 | setTopPosition(height: number) { 291 | if (this.parentType === 'table') { 292 | this.content.parentElement.style.top = `${height}px`; 293 | } else { 294 | this.content.style.top = `${height}px`; 295 | } 296 | } 297 | 298 | private getCachedViewsBufferSize(): number { 299 | let ret = 0; 300 | 301 | if (this.cachedViewsBufferSize) { 302 | ret = this.cachedViewsBufferSize; 303 | } else { 304 | if (this._lastViewPort) { 305 | ret = (this._lastViewPort.end - this._lastViewPort.start) + 1; 306 | } 307 | } 308 | 309 | return ret; 310 | } 311 | 312 | private cacheView(view: EmbeddedViewRef>) { 313 | if (view == null) { return; } 314 | if (this.getCachedViewsBufferSize() >= this._cachedViews.length) { 315 | this._cachedViews.push(view); 316 | if (view.context !== null && view.context !== undefined) { 317 | view.context.$implicit = null; 318 | } 319 | } else { 320 | view.destroy(); 321 | } 322 | } 323 | 324 | private getNewView(): EmbeddedViewRef> { 325 | let ret: EmbeddedViewRef> = null; 326 | 327 | if (this._cachedViews.length > 0) { 328 | ret = this._cachedViews.pop(); 329 | } 330 | 331 | return ret; 332 | } 333 | 334 | private renderViewPort(topPosition: number, start: number, end: number) { 335 | this.topPosition = topPosition; 336 | const vp = new ViewPort(topPosition, Math.min(start, this.length), Math.min(end, this.length)); 337 | const lastViewPort = this._lastViewPort; 338 | this._lastViewPort = vp; 339 | const lastViewPortExtent = lastViewPort.end - lastViewPort.start + 1; 340 | const vpExtent = vp.end - vp.start + 1; 341 | const frontAdd = Math.min(vpExtent, lastViewPort.start - vp.start); 342 | const frontRemove = Math.min(vp.start - lastViewPort.start, lastViewPortExtent); 343 | const backRemove = Math.min(lastViewPortExtent, lastViewPort.end - vp.end); 344 | const backAdd = Math.min(vpExtent, vp.end - lastViewPort.end); 345 | const skip = Math.max(0, Math.min(lastViewPort.end + 1, vp.end + 1) - Math.max(lastViewPort.start, vp.start)); 346 | 347 | if (frontAdd || frontRemove || backRemove || backAdd) { 348 | // Remove all the items at the end that are now out of viewport scope (backremove>0)... 349 | if (backRemove > 0) { 350 | for (let loop = lastViewPortExtent; loop > lastViewPortExtent - backRemove; loop--) { 351 | this.cacheView(this._viewContainer.detach(loop) as EmbeddedViewRef>); 352 | } 353 | } 354 | // Now remove all the front items that are now out of viewport scope (frontRemove>0)... 355 | if (frontRemove > 0) { 356 | for (let loop = 0; loop < frontRemove; loop++) { 357 | this.cacheView(this._viewContainer.detach(0) as EmbeddedViewRef>); 358 | } 359 | } 360 | 361 | // Ok, so now we need to seek to the start point of the new viewport 362 | let it = null; 363 | let eof = false; 364 | if (!Array.isArray(this._iterable)) { 365 | // have to do a manual loop based seek.. 366 | it = this._iterable[Symbol.iterator]; 367 | if (it !== null && it !== undefined && it.seekTo !== undefined) { 368 | (it).seekTo(vp.start); 369 | } 370 | let item: any; 371 | for (let loop = 0; loop < vp.start; loop++) { 372 | item = it.next(); 373 | if (item.done) { 374 | eof = true; 375 | break; 376 | } 377 | } 378 | } 379 | 380 | let indexPoint = 0; 381 | 382 | // Do we need to insert items in the front ? 383 | if (frontAdd > 0 && !eof) { 384 | indexPoint += frontAdd; 385 | for (let loop = 0; loop < frontAdd; loop++) { 386 | let item: any; 387 | if (it === null) { 388 | if (this._iterable['length'] <= vp.start + loop) { 389 | eof = true; 390 | break; 391 | } 392 | item = this._iterable[vp.start + loop]; 393 | } else { 394 | const e = it.next(); 395 | if (e.done) { 396 | eof = true; 397 | break; 398 | } else { 399 | item = e.value; 400 | } 401 | } 402 | if (!eof && item !== null && item !== undefined) { 403 | let view = this.getNewView(); 404 | if (view) { 405 | this._viewContainer.insert(view, loop); 406 | view.context.$implicit = item; 407 | } else { 408 | view = this._viewContainer.createEmbeddedView( 409 | this._template, new NgForOfContext(item, this._iterable, -1, -1), loop); 410 | } 411 | } 412 | } 413 | } 414 | 415 | // now skip the items we already have rendered 416 | indexPoint += skip; 417 | if (!eof && skip > 0 && it !== null) { 418 | for (let loop = 0; loop < skip; loop++) { 419 | const e = it.next(); 420 | if (e.done) { 421 | eof = true; 422 | break; 423 | } 424 | } 425 | } 426 | 427 | // Do we have items to add at the back ? 428 | if (!eof && backAdd > 0) { 429 | for (let loop = 0; loop < backAdd; loop++) { 430 | let item: any; 431 | if (it !== null) { 432 | const e = it.next(); 433 | if (e.done) { 434 | eof = true; 435 | break; 436 | } 437 | item = e.value; 438 | } else { 439 | item = this._iterable[vp.start + indexPoint + loop]; 440 | } 441 | if (!eof && item !== null && item !== undefined) { 442 | let view = this.getNewView(); 443 | if (view) { 444 | this._viewContainer.insert(view); 445 | view.context.$implicit = item; 446 | } else { 447 | view = this._viewContainer.createEmbeddedView( 448 | this._template, new NgForOfContext(item, this._iterable, -1, -1)); 449 | } 450 | } 451 | } 452 | } 453 | 454 | for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) { 455 | const viewRef = >>this._viewContainer.get(i); 456 | if (viewRef !== null && viewRef !== undefined) { 457 | viewRef.context.index = i + vp.start; 458 | viewRef.context.count = this._length; 459 | viewRef.context.ngForOf = this._iterable; 460 | } 461 | } 462 | } 463 | 464 | this.viewPortUpdated.emit(new NgVForRenderUpdatedArgs(vp.topPosition, vp.start, vp.end)); 465 | } 466 | 467 | // end region Implementation 468 | 469 | } 470 | --------------------------------------------------------------------------------