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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------