Skip to content

Commit

Permalink
Merge pull request #169 from dhilt/issue-168-input-data-unification
Browse files Browse the repository at this point in the history
Input data unification
  • Loading branch information
dhilt authored Apr 21, 2020
2 parents 6f58be8 + cbeeb9b commit 1abb97d
Show file tree
Hide file tree
Showing 38 changed files with 868 additions and 721 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Unlimited bidirectional scrolling over limited viewport. A directive for [Angula

### Motivation

Scrolling large date sets may cause performance issues. Many DOM elements, many data-bindings, many event listeners... The common way to improve this case is to render only a small portion of the data set visible to a user. Other data set elements that are not visible to a user are virtualized with upward and downward empty padding elements which should give us a consistent viewport with consistent scrollbar parameters.
Scrolling large data sets may cause performance issues. Many DOM elements, many data-bindings, many event listeners... The common way to improve this case is to render only a small portion of the data set visible to a user. Other data set elements that are not visible to a user are virtualized with upward and downward empty padding elements which should give us a consistent viewport with consistent scrollbar parameters.

The \*uiScroll is structural directive that works like \*ngFor and renders a templated element once per item from a collection. By requesting the external Datasource (the implementation of which is a developer responsibility) the \*uiScroll directive fetches necessary portion of the data set and renders corresponded elements until the visible part of the viewport is filled out. It starts to retrieve new data to render new elements again if a user scrolls to the edge of visible element list. It dynamically destroys elements as they become invisible and recreates them if they become visible again.
<p align="center">
Expand Down Expand Up @@ -182,7 +182,7 @@ Below is the list of invocable methods of the Adapter API.
|[prepend](https://dhilt.github.io/ngx-ui-scroll/#/adapter#append-prepend)|(items:&nbsp;any&nbsp;&vert;&nbsp;any[], bof?:&nbsp;boolean)|Adds items or single item to the beginning of the uiScroll dataset. If bof parameter is not set, items will be added and rendered immediately, they will be placed right before the first item in the uiScroll buffer. If bof parameter is set to true, items will be added and rendered only if the beginning of the dataset is reached; otherwise, these items will be virtualized. |
|[check](https://dhilt.github.io/ngx-ui-scroll/#/adapter#check-size)| |Checks if any of current items changed it's size and runs a procedure to provide internal consistency and new items fetching if needed. |
|[remove](https://dhilt.github.io/ngx-ui-scroll/#/adapter#remove)|(predicate:&nbsp;ItemsPredicate)<br><br>type&nbsp;ItemsPredicate&nbsp;=<br>&nbsp;&nbsp;(item: ItemAdapter)&nbsp;=><br>&nbsp;&nbsp;&nbsp;&nbsp;boolean|Removes items from current buffer. Predicate is a function to be applied to every item presently in the buffer. Predicate must return boolean value. If predicate's return value is true, the item will be removed. _Note!_ Current implementation allows to remove only a continuous series of items per call. If you want to remove, say, 5 and 7 items, you should call the remove method twice. Removing a series of items from 5 to 7 could be done in a single call. |
|[clip](https://dhilt.github.io/ngx-ui-scroll/#/adapter#clip)|(options: {<br>&nbsp;&nbsp;forwardOnly?:&nbsp;boolean,<br>&nbsp;&nbsp;backwardOnly?:&nbsp;boolean<br>})|Removes out-of-viewport items on demand. The direction in which invisible items should be clipped can be specified by passing an options object. If no options is passed, clipping will affect both forward and backward directions. |
|[clip](https://dhilt.github.io/ngx-ui-scroll/#/adapter#clip)|(options: {<br>&nbsp;&nbsp;forwardOnly?:&nbsp;boolean,<br>&nbsp;&nbsp;backwardOnly?:&nbsp;boolean<br>})|Removes out-of-viewport items on demand. The direction in which invisible items should be clipped can be specified by passing an options object. If no options is passed (or both properties are set to _true_), clipping will occur in both directions. |
|[insert](https://dhilt.github.io/ngx-ui-scroll/#/adapter#insert)|(options: {<br>&nbsp;&nbsp;items:&nbsp;any[],<br>&nbsp;&nbsp;before?:&nbsp;ItemsPredicate,<br>&nbsp;&nbsp;after?:&nbsp;ItemsPredicate,<br>&nbsp;&nbsp;decrease?:&nbsp;boolean<br>})|Inserts items _before_ or _after_ the one that satisfies the predicate condition. Only one of _before_ and _after_ options is allowed. Indexes increase by default. Decreasing strategy can be enabled via _decrease_ option. |

Along with the documented API there are some undocumented features that can be treated as experimental. They are not tested enough and might change over time. Some of them can be found on the [experimental tab](https://dhilt.github.io/ngx-ui-scroll/#/experimental) of the demo app.
Expand Down
2 changes: 1 addition & 1 deletion demo/app/samples/adapter/reset.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<p>
All fields of the <em>datasource</em> argument are optional.
Missing parts of a new Datasource will be taken from the original one.
For instance, in this demo only <em>settings</em> field are passed:
For instance, in this demo only <em>settings</em> field is passed:
<em>Adapter.reset(&#123; settings &#125;)</em>.
This sets new <em>startIndex</em> and <em>bufferSize</em> settings
while <em>Datasource.get</em> remains pristine.
Expand Down
2 changes: 1 addition & 1 deletion package-dist.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-ui-scroll",
"version": "1.6.0",
"version": "1.6.1",
"description": "Infinite/virtual scroll for Angular",
"main": "./bundles/ngx-ui-scroll.umd.js",
"module": "./fesm5/ngx-ui-scroll.js",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"deploy-app": "npm run build-app && firebase deploy",
"preinstall": "cd server && npm install",
"postinstall": "npm run build-app",
"pack:install": "npm run build && npm pack ./dist && npm install ngx-ui-scroll-1.6.0.tgz --no-save",
"pack:install": "npm run build && npm pack ./dist && npm install ngx-ui-scroll-1.6.1.tgz --no-save",
"pack:start": "npm run pack:install && npm start",
"build": "node build.js",
"publish:lib": "npm run build && npm publish ./dist",
Expand Down
8 changes: 4 additions & 4 deletions src/component/classes/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { takeUntil } from 'rxjs/operators';
import { Scroller } from '../scroller';
import { Logger } from './logger';
import { Buffer } from './buffer';
import { AdapterContext } from './adapterContext';
import { ADAPTER_PROPS } from '../utils/index';
import { AdapterContext, EMPTY_ITEM } from './adapter/context';
import { ADAPTER_PROPS } from './adapter/props';
import {
WorkflowGetter,
AdapterPropType,
Expand All @@ -23,7 +23,7 @@ import {
IDatasourceOptional
} from '../interfaces/index';

const ADAPTER_PROPS_STUB = ADAPTER_PROPS();
const ADAPTER_PROPS_STUB = ADAPTER_PROPS(EMPTY_ITEM);

const fixScalarWanted = (name: string, container: { [key: string]: boolean }) => {
const scalar = ADAPTER_PROPS_STUB.find(
Expand Down Expand Up @@ -75,7 +75,7 @@ export class Adapter implements IAdapter {
...prop,
value: (publicContext as any)[prop.name]
}))
: ADAPTER_PROPS();
: ADAPTER_PROPS(EMPTY_ITEM);

// Scalar permanent props
adapterProps
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { ADAPTER_PROPS, itemAdapterEmpty } from '../utils/index';
import { AdapterPropType } from '../interfaces/index';
import { ADAPTER_PROPS } from './props';
import { ItemAdapter, AdapterPropType } from '../../interfaces/index';

let instanceCount = 0;

export const EMPTY_ITEM = {
data: {},
element: {}
} as ItemAdapter;

export class AdapterContext {
id: number;
mock: boolean;
Expand All @@ -11,7 +16,7 @@ export class AdapterContext {
const id = ++instanceCount;

// props will be reassigned on Scroller instantiation
ADAPTER_PROPS().forEach(({ name, value, type, permanent }) =>
ADAPTER_PROPS(EMPTY_ITEM).forEach(({ name, value, type, permanent }) =>
Object.defineProperty(this, name, {
get: () => value,
configurable: !mock || permanent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
import { BehaviorSubject, Subject } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';

import { VALIDATORS } from './validation';
import {
AdapterPropType as Prop,
IAdapterProp,
ItemAdapter,
IAdapter,
IAdapterMethodParam,
IAdapterMethodParams,
IAdapterMethods
} from '../interfaces/index';

export const itemAdapterEmpty = {
data: {},
element: {}
} as ItemAdapter;
import { AdapterPropType as Prop, IAdapterProp } from '../../interfaces/index';

const noop = () => null;

export const ADAPTER_PROPS = (): IAdapterProp[] => [
export const ADAPTER_PROPS = (nullItem: any): IAdapterProp[] => [
{
type: Prop.Scalar,
name: 'id',
Expand Down Expand Up @@ -59,14 +44,14 @@ export const ADAPTER_PROPS = (): IAdapterProp[] => [
{
type: Prop.Scalar,
name: 'firstVisible',
value: itemAdapterEmpty,
value: nullItem,
observable: 'firstVisible$',
wanted: true
},
{
type: Prop.Scalar,
name: 'lastVisible',
value: itemAdapterEmpty,
value: nullItem,
observable: 'lastVisible$',
wanted: true
},
Expand Down Expand Up @@ -156,12 +141,12 @@ export const ADAPTER_PROPS = (): IAdapterProp[] => [
{
type: Prop.Observable,
name: 'firstVisible$',
value: new BehaviorSubject<ItemAdapter>(itemAdapterEmpty)
value: new BehaviorSubject<any>(nullItem)
},
{
type: Prop.Observable,
name: 'lastVisible$',
value: new BehaviorSubject<ItemAdapter>(itemAdapterEmpty)
value: new BehaviorSubject<any>(nullItem)
},
{
type: Prop.Observable,
Expand All @@ -174,73 +159,3 @@ export const ADAPTER_PROPS = (): IAdapterProp[] => [
value: new Subject<boolean>()
}
];

const {
MANDATORY,
INTEGER_UNLIMITED,
BOOLEAN,
OBJECT,
ITEM_LIST,
FUNC_WITH_X_ARGUMENTS,
FUNC_WITH_X_AND_MORE_ARGUMENTS,
ONE_OF_MUST,
} = VALIDATORS;

const FIX_METHOD_PARAMS: IAdapterMethodParams = {
scrollPosition: {
name: 'scrollPosition',
validators: [INTEGER_UNLIMITED]
},
minIndex: {
name: 'minIndex',
validators: [INTEGER_UNLIMITED]
},
maxIndex: {
name: 'maxIndex',
validators: [INTEGER_UNLIMITED]
},
updater: {
name: 'updater',
validators: [FUNC_WITH_X_ARGUMENTS(1)]
}
};

const INSERT_METHOD_PARAMS: IAdapterMethodParams = {
items: {
name: 'items',
validators: [MANDATORY, ITEM_LIST]
},
before: {
name: 'before',
validators: [FUNC_WITH_X_ARGUMENTS(1), ONE_OF_MUST(['after'])]
},
after: {
name: 'after',
validators: [FUNC_WITH_X_ARGUMENTS(1), ONE_OF_MUST(['before'])]
},
decrease: {
name: 'decrease',
validators: [BOOLEAN]
}
};

const RESET_METHOD_PARAMS: IAdapterMethodParams = {
get: {
name: 'get',
validators: [FUNC_WITH_X_AND_MORE_ARGUMENTS(2)]
},
settings: {
name: 'settings',
validators: [OBJECT]
},
devSettings: {
name: 'devSettings',
validators: [OBJECT]
}
};

export const ADAPTER_METHODS_PARAMS: IAdapterMethods = {
FIX: FIX_METHOD_PARAMS,
INSERT: INSERT_METHOD_PARAMS,
RESET: RESET_METHOD_PARAMS,
};
2 changes: 1 addition & 1 deletion src/component/classes/buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export class Buffer {
}

get hasItemSize(): boolean {
return this.averageSize !== void 0;
return this.averageSize > 0;
}

get minIndex(): number {
Expand Down
10 changes: 8 additions & 2 deletions src/component/classes/datasource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IDatasource, DatasourceGet, DevSettings, Settings, IAdapter } from '../interfaces/index';
import { AdapterContext } from './adapterContext';
import { AdapterContext } from './adapter/context';

export class Datasource implements IDatasource {
get: DatasourceGet;
Expand All @@ -8,7 +8,13 @@ export class Datasource implements IDatasource {
adapter: IAdapter;

constructor(datasource: IDatasource, mockAdapter?: boolean) {
Object.assign(this as any, datasource);
this.get = datasource.get;
if (datasource.settings) {
this.settings = datasource.settings;
}
if (datasource.devSettings) {
this.devSettings = datasource.devSettings;
}
this.adapter = (new AdapterContext(mockAdapter)) as IAdapter;
}
}
20 changes: 16 additions & 4 deletions src/component/classes/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ export class Logger {
'range: ' + (first && last ? `[${first.$index}..${last.$index}]` : 'no');
};
this.getFetchRange = (): string => {
const { firstIndex, lastIndex } = scroller.state.fetch;
const hasInterval = firstIndex !== null && lastIndex !== null && !isNaN(firstIndex) && !isNaN(lastIndex);
return hasInterval ? `[${firstIndex}..${lastIndex}]` : 'no';
const { firstIndex: first, lastIndex: last } = scroller.state.fetch;
return first !== null && last !== null && !Number.isNaN(first) && !Number.isNaN(last)
? `[${first}..${last}]`
: 'no';
};
this.getLoop = (): string => scroller.state.loop;
this.getLoopNext = (): string => scroller.state.loopNext;
Expand All @@ -53,7 +54,18 @@ export class Logger {
this.log(() => [
str,
stringify
? JSON.stringify(obj)
? JSON.stringify(obj, (k, v) => {
if (Number.isNaN(v)) {
return 'NaN';
}
if (v === Infinity) {
return 'Infinity';
}
if (v === -Infinity) {
return '-Infinity';
}
return v;
})
.replace(/"/g, '')
.replace(/(\{|\:|\,)/g, '$1 ')
.replace(/(\})/g, ' $1')
Expand Down
72 changes: 23 additions & 49 deletions src/component/classes/settings.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,5 @@
import { Settings as ISettings, DevSettings as IDevSettings } from '../interfaces/index';
import { assignSettings, assignDevSettings } from '../utils/index';

export const defaultSettings: ISettings = {
adapter: false,
startIndex: 1,
minIndex: -Infinity,
maxIndex: Infinity,
bufferSize: 5,
padding: 0.5,
infinite: false,
horizontal: false,
windowViewport: false,
inverse: false // [experimental] if true, backward padding element will have a priority when filling the viewport in case of lack of items
};

export const minSettings: ISettings = {
itemSize: 1,
bufferSize: 1,
padding: 0.01
};

export const defaultDevSettings: IDevSettings = {
debug: false, // if true, logging is enabled; need to turn off when release
immediateLog: true, // if false, logging is not immediate and could be done via Workflow.logForce call
logTime: false, // if true, time differences will be logged
logProcessRun: false, // if true, process fire/run info will be logged
throttle: 40, // if > 0, scroll event handling is throttled (ms)
initDelay: 1, // if set, the Workflow initialization will be postponed (ms)
initWindowDelay: 40, // if set and the entire window is scrollable, the Workflow init will be postponed (ms)
changeOverflow: false, // if true, scroll will be disabled per each item's average size change
};

export const minDevSettings: IDevSettings = {
throttle: 0,
initDelay: 0,
initWindowDelay: 0
};
import { Settings as ISettings, DevSettings as IDevSettings, ICommonProps } from '../interfaces/index';
import { SETTINGS, DEV_SETTINGS, validate } from '../inputs/index';

export class Settings implements ISettings, IDevSettings {

Expand All @@ -50,17 +14,17 @@ export class Settings implements ISettings, IDevSettings {
infinite: boolean;
horizontal: boolean;
windowViewport: boolean;
inverse: boolean; // if true, bwd padding element will have a priority when filling the viewport (if lack of items)

// development settings
debug: boolean;
immediateLog: boolean;
logTime: boolean;
logProcessRun: boolean;
throttle: number;
initDelay: number;
initWindowDelay: number;
changeOverflow: boolean;
inverse: boolean;
debug: boolean; // if true, logging is enabled; need to turn off when release
immediateLog: boolean; // if false, logging is not immediate and could be done via Workflow.logForce call
logTime: boolean; // if true, time differences will be logged
logProcessRun: boolean; // if true, process fire/run info will be logged
throttle: number; // if > 0, scroll event handling is throttled (ms)
initDelay: number; // if set, the Workflow initialization will be postponed (ms)
initWindowDelay: number; // if set and the entire window is scrollable, the Workflow init will be postponed (ms)
changeOverflow: boolean; // if true, scroll will be disabled per each item's average size change

// internal settings, managed by scroller itself
instanceIndex: number;
Expand All @@ -69,13 +33,23 @@ export class Settings implements ISettings, IDevSettings {
constructor(
settings: ISettings | undefined, devSettings: IDevSettings | undefined, instanceIndex: number
) {
assignSettings(this, settings || {}, defaultSettings, minSettings);
assignDevSettings(this, devSettings || {}, defaultDevSettings, minDevSettings);
this.parseInput(settings, SETTINGS);
this.parseInput(devSettings, DEV_SETTINGS);
this.instanceIndex = instanceIndex;
this.initializeDelay = this.getInitializeDelay();
// todo: min/max indexes must be ignored if infinite mode is enabled ??
}

parseInput(input: ISettings | IDevSettings | undefined, props: ICommonProps<any>) {
const result = validate(input, props);
if (!result.isValid) {
throw new Error('Invalid settings');
}
Object.entries(result.params).forEach(([key, par]) =>
Object.assign(this, { [key]: par.value })
);
}

getInitializeDelay(): number {
let result = 0;
if (this.windowViewport && this.initWindowDelay && !('scrollRestoration' in history)) {
Expand Down
Loading

0 comments on commit 1abb97d

Please sign in to comment.