From 28778f972f38da268efe8b7ba8e9071c394aea3a Mon Sep 17 00:00:00 2001 From: Andrei Fangli Date: Sun, 21 Apr 2024 13:01:00 +0300 Subject: [PATCH] Work in progress, refactoring observable collections --- package-lock.json | 4 +- package.json | 7 +- src/events.ts | 62 +- src/form-field-collection-view-model.ts | 111 +-- src/hooks/use-observable-collection.ts | 4 +- src/index.ts | 2 +- src/observable-collection.ts | 639 ++++++++++-------- src/validation.ts | 4 +- .../components/input-tests.tsx | 0 {tests => tests-old}/events-tests.ts | 0 .../form-field-collection-view-model-tests.ts | 0 .../form-field-view-model-tests.ts | 0 .../hooks/use-view-model-factory-tests.tsx | 0 .../hooks/use-view-model-type-tests.tsx | 0 .../hooks/watch-collection-tests.tsx | 0 .../hooks/watch-event-tests.tsx | 0 .../hooks/watch-view-model-tests.tsx | 0 .../observable-collection-tests.ts | 0 {tests => tests-old}/validation-tests.ts | 0 {tests => tests-old}/view-model-tests.ts | 0 .../common/expectCollectionsToBeEqual.ts | 36 + tests/observable-collection/common/index.ts | 2 + .../common/testBlankMutatingOperation.ts | 48 ++ .../common/testMutatingOperation.ts | 62 ++ .../observable-collection.pop.tests.ts | 37 + .../observable-collection.push.tests.ts | 51 ++ tsconfig.json | 4 +- 27 files changed, 667 insertions(+), 406 deletions(-) rename {tests => tests-old}/components/input-tests.tsx (100%) rename {tests => tests-old}/events-tests.ts (100%) rename {tests => tests-old}/form-field-collection-view-model-tests.ts (100%) rename {tests => tests-old}/form-field-view-model-tests.ts (100%) rename {tests => tests-old}/hooks/use-view-model-factory-tests.tsx (100%) rename {tests => tests-old}/hooks/use-view-model-type-tests.tsx (100%) rename {tests => tests-old}/hooks/watch-collection-tests.tsx (100%) rename {tests => tests-old}/hooks/watch-event-tests.tsx (100%) rename {tests => tests-old}/hooks/watch-view-model-tests.tsx (100%) rename {tests => tests-old}/observable-collection-tests.ts (100%) rename {tests => tests-old}/validation-tests.ts (100%) rename {tests => tests-old}/view-model-tests.ts (100%) create mode 100644 tests/observable-collection/common/expectCollectionsToBeEqual.ts create mode 100644 tests/observable-collection/common/index.ts create mode 100644 tests/observable-collection/common/testBlankMutatingOperation.ts create mode 100644 tests/observable-collection/common/testMutatingOperation.ts create mode 100644 tests/observable-collection/observable-collection.pop.tests.ts create mode 100644 tests/observable-collection/observable-collection.push.tests.ts diff --git a/package-lock.json b/package-lock.json index 1581657..6bb37d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-model-view-viewmodel", - "version": "3.0.0-events", + "version": "3.0.0-events.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-model-view-viewmodel", - "version": "3.0.0-events", + "version": "3.0.0-events.1", "license": "MIT", "devDependencies": { "@testing-library/react": "^14.2.1", diff --git a/package.json b/package.json index 34e728c..d905356 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-model-view-viewmodel", - "version": "3.0.0-events.1", + "version": "3.0.0-collections", "description": "A library for developing React applications using Model-View-ViewModel inspired by .NET", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -55,8 +55,9 @@ "preset": "ts-jest", "testEnvironment": "jsdom", "testMatch": [ - "**/tests/**/*.ts", - "**/tests/**/*.tsx" + "**/*.tests.ts", + "**/tests/**/*.tests.ts", + "**/tests/**/*.tests.tsx" ] } } diff --git a/src/events.ts b/src/events.ts index b2b2b47..7a86ebe 100644 --- a/src/events.ts +++ b/src/events.ts @@ -71,14 +71,6 @@ export class EventDispatcher implements IEvent extends EventDispatcher { -} - /** A core interface for objects that notify subscribers when their properties have changed. Components can react to this and display the new value as a consequence. */ export interface INotifyPropertiesChanged { /** An event that is raised when one or more properties may have changed. */ @@ -113,66 +105,36 @@ export interface IPropertiesChangedEventHandler extends IEventHandler { - /** An event that is raised when an item is added to the collection. */ - readonly itemAdded: IEvent>; - - /** An event that is raised when an item is removed from the collection. */ - readonly itemRemoved: IEvent>; - /** An event that is raised when the collection changed. */ readonly collectionChanged: ICollectionChangedEvent; } -/** - * Contains information about the item that was added to a collection, including the option to subscribe a clean-up callback. - * @template TItem The type of items the collection contains. - */ -export interface IItemAddedEventArgs { - /** The item that was added. */ - readonly item: TItem; - /** The index where the item was added. */ - readonly index: number; - - /** - * Subscribes the provided `callback` that will be executed when the item is removed. - * @param callback A callback that will be invoked when the item is removed. - */ - addItemRemovalCallback(callback: ItemRemovedCallback): void; -} - -/** - * Represents an item removal callback. - * @template TItem The type of items the collection contains. - * @param item The item that was removed from the collection. - * @param index The index from which the item was removed. -*/ -export type ItemRemovedCallback = (item: TItem, index: number) => void; - -/** - * Contains information about the item that was removed from a collection. - * @template TItem The type of items the collection contains. - */ -export interface IItemRemovedEventArgs { - /** The item that was removed. */ - readonly item: TItem; - /** The index from which the item was removed. */ - readonly index: number; -} - /** * Contains information about the changes in the collection. * @template TItem The type of items the collection contains. */ export interface ICollectionChange { + /** The start index where the change has happenend. */ + readonly startIndex: number; /** An array of added items, if any. */ readonly addedItems: readonly TItem[]; /** An array of removed items, if any. */ readonly removedItems: readonly TItem[]; + /** The operation that was performed */ + readonly operation: CollectionOperation; } +/** + * Describes all the possible operations that can be performed on a collection. + */ +export type CollectionOperation = 'push' | 'pop' | 'unshift' | 'shift' | 'sort' | 'reverse' | 'copyWithin' | 'set' | 'delete' | 'fill' | 'clear' | 'reset'; + /** * A specialized event for subscribing and unsubscribing from collection changed events. * @template TSubject Provides the object that raised the event. diff --git a/src/form-field-collection-view-model.ts b/src/form-field-collection-view-model.ts index 598667e..a76858c 100644 --- a/src/form-field-collection-view-model.ts +++ b/src/form-field-collection-view-model.ts @@ -26,21 +26,25 @@ export abstract class FormFieldCollectionViewModel = new ObservableCollection(); - private readonly _fieldChangedEventHandler: IPropertiesChangedEventHandler; + private readonly _fields: IObservableCollection; /** Initializes a new instance of the {@link FormFieldCollectionViewModel} class. */ public constructor() { super(); - this._fieldChangedEventHandler = { - handle: (_, changedProperties) => { - if (changedProperties.indexOf('isValid') >= 0 || changedProperties.indexOf('isInvalid') >= 0) - this.notifyPropertiesChanged('isValid', 'isInvalid'); - } + + const fieldChangedEventHandler: IPropertiesChangedEventHandler = { + handle: this.onFieldChanged.bind(this) } + this._fields = new ObservableCollection(); + this._fields.collectionChanged.subscribe({ + handle(_, { addedItems: addedFields, removedItems: removedFields }) { + addedFields.forEach(addedField => addedField.propertiesChanged.subscribe(fieldChangedEventHandler)); + removedFields.forEach(removedField => removedField.propertiesChanged.unsubscribe(fieldChangedEventHandler)); + } + }) } - /** A collection of registered fields. */ + /** A collection containing the registered fields. */ public get fields(): IReadOnlyObservableCollection { return this._fields; } @@ -56,7 +60,7 @@ export abstract class FormFieldCollectionViewModel(name: string, initialValue: TValue): TFormFieldViewModel { - return this.registerField(new FormFieldViewModel(name, initialValue) as any as TFormFieldViewModel); - } - - /** Registers the provided field and returns it. - * @param field The field to register. - * @returns Returns the provided field that has been registered. - */ - protected registerField(field: TFormFieldViewModel): TFormFieldViewModel { - this.registerFields(field); - return field; - } - - /** Registers the provided fields. + /** + * Registers the provided fields. * @param fields The fields to register. */ - protected registerFields(...fields: readonly TFormFieldViewModel[]): void { - fields.forEach(field => { - field.propertiesChanged.subscribe(this._fieldChangedEventHandler); - this._fields.push(field); - }); - - if (fields.length > 0) + protected registerFields(...fields: readonly (TFormFieldViewModel | readonly TFormFieldViewModel[])[]): void { + const previousFieldCount = this._fields.length; + + const currentFieldCount = this._fields.push(...fields.reduce( + (reuslt, fieldsOrArrays) => { + if (Array.isArray(fieldsOrArrays)) + reuslt.push(...fieldsOrArrays) + else + reuslt.push(fieldsOrArrays as TFormFieldViewModel); + return reuslt + }, + [] + )); + + if (previousFieldCount !== currentFieldCount) this.notifyPropertiesChanged('isValid', 'isInvalid'); } - /** Unregisters the provided field. - * @param field The previously registered field. - */ - protected unregisterField(field: TFormFieldViewModel): void { - this.unregisterFields(field); - } - - /** Unregisters the provided fields. + /** + * Unregisters the provided fields. * @param fields The previously registered fields. */ - protected unregisterFields(...fields: readonly TFormFieldViewModel[]): void { + protected unregisterFields(...fields: readonly (TFormFieldViewModel | readonly TFormFieldViewModel[])[]): void { let hasUnregisteredFields = false; - fields.forEach(field => { + + const removeField = (field: TFormFieldViewModel) => { const indexToRemove = this._fields.indexOf(field); if (indexToRemove >= 0) { - const removedField = this._fields[indexToRemove]; - removedField.propertiesChanged.unsubscribe(this._fieldChangedEventHandler); this._fields.splice(indexToRemove, 1); hasUnregisteredFields = true; } + } + + fields.forEach(fieldsOrArrays => { + if (Array.isArray(fieldsOrArrays)) + fieldsOrArrays.forEach(removeField) + else + removeField(fieldsOrArrays as TFormFieldViewModel); }); if (hasUnregisteredFields) this.notifyPropertiesChanged('isValid', 'isInvalid'); } + + /** + * Called when one of the registered fields notifies about changed properties. + * @param field the field that has changed. + * @param changedProperties the properties that have changed. + */ + protected onFieldChanged(field: TFormFieldViewModel, changedProperties: readonly (keyof TFormFieldViewModel)[]): void { + if (changedProperties.indexOf('isValid') >= 0 || changedProperties.indexOf('isInvalid') >= 0) + this.notifyPropertiesChanged('isValid', 'isInvalid'); + } } -/** A helper class for creating forms, can be extended or reused to implement a similar feature to {@link FormFieldCollectionViewModel.create}. +/** + * A helper class for creating forms, can be extended or reused to implement a similar feature to {@link FormFieldCollectionViewModel.create}. * @template TFormFieldViewModel The type of fields the form collection contains, defaults to {@link FormFieldViewModel}. * @template TFormFields the set of fields to register on the form. */ export class DynamicFormFieldCollectionViewModel, TFormFields extends FormFieldSet> extends FormFieldCollectionViewModel { - /** Initializes a new instance of the {@link DynamicFormFieldCollectionViewModel} class. + /** + * Initializes a new instance of the {@link DynamicFormFieldCollectionViewModel} class. * @param fields The form fields. */ public constructor(fields: TFormFields) { super(); + const formFields: TFormFieldViewModel[] = []; Object.getOwnPropertyNames(fields).forEach( fieldPropertyName => { + const formField = fields[fieldPropertyName]; + formFields.push(formField); Object.defineProperty( this, fieldPropertyName, @@ -147,10 +157,11 @@ export class DynamicFormFieldCollectionViewModel(observableCollection: IReadOnlyOb ); } -function hasChanges(previous: readonly TItem[], next: readonly TItem[]): boolean { - return previous.length !== next.length || previous.some((item, index) => item !== next[index]); +function hasChanges(previous: readonly TItem[], next: IReadOnlyObservableCollection): boolean { + return previous.length !== next.length || previous.some((item, index) => item !== next.at(index)); } /** Watches the collection for changes, requesting a render when it does. The collection is the only hook dependency. diff --git a/src/index.ts b/src/index.ts index f8717fe..5fd1616 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export { type IEvent, type IEventHandler, type INotifyPropertiesChanged, type IPropertiesChangedEvent, type IPropertiesChangedEventHandler, type INotifyCollectionChanged, type ICollectionChangedEvent, type ICollectionChangedEventHandler, type IItemAddedEventArgs, type IItemRemovedEventArgs, type ItemRemovedCallback, type ICollectionChange, EventDispatcher, DispatchEvent } from './events'; +export { type IEvent, type IEventHandler, type INotifyPropertiesChanged, type IPropertiesChangedEvent, type IPropertiesChangedEventHandler, type INotifyCollectionChanged, type ICollectionChangedEvent, type ICollectionChangedEventHandler, type ICollectionChange, EventDispatcher } from './events'; export { ViewModel, isViewModel } from './view-model'; diff --git a/src/observable-collection.ts b/src/observable-collection.ts index af3b073..ce1f156 100644 --- a/src/observable-collection.ts +++ b/src/observable-collection.ts @@ -1,15 +1,88 @@ -import type { ICollectionChange, ICollectionChangedEvent, IEvent, IItemAddedEventArgs, IItemRemovedEventArgs, INotifyCollectionChanged, INotifyPropertiesChanged, ItemRemovedCallback } from './events'; +import type { CollectionOperation, ICollectionChange, ICollectionChangedEvent, INotifyCollectionChanged, INotifyPropertiesChanged } from './events'; import { EventDispatcher } from './events'; import { ViewModel } from './view-model'; /** Represents a read-only observable collection based on the read-only array interface. * @template TItem The type of items the collection contains. */ -export interface IReadOnlyObservableCollection extends Readonly, INotifyPropertiesChanged, INotifyCollectionChanged { - /** Converts the observable collection to a native JavaScript {@link Array}. +export interface IReadOnlyObservableCollection extends Iterable, INotifyPropertiesChanged, INotifyCollectionChanged { + readonly length: number; + + readonly [index: number]: TItem; + + at(index: number): TItem; + with(index: number, item: TItem): TItem[]; + + entries(): IterableIterator<[number, TItem]>; + keys(): IterableIterator; + values(): IterableIterator; + + forEach(callback: (item: TItem, index: number, colleciton: this) => void): void; + forEach(callback: (this: TContext, item: TItem, index: number, colleciton: this) => void, thisArg: TContext): void; + + includes(item: TItem): boolean; + includes(item: TItem, fromIndex: number): boolean; + + indexOf(item: TItem): number; + indexOf(item: TItem, fromIndex: number): number; + + lastIndexOf(item: TItem): number; + lastIndexOf(item: TItem, fromIndex: number): number; + + findIndex(callback: (item: TItem, index: number, colleciton: this) => boolean): number; + findIndex(callback: (this: TContext, item: TItem, index: number, colleciton: this) => boolean, thisArg: TContext): number; + + findLastIndex(callback: (item: TItem, index: number, colleciton: this) => boolean): number; + findLastIndex(callback: (this: TContext, item: TItem, index: number, colleciton: this) => boolean, thisArg: TContext): number; + + find(callback: (item: TItem, index: number, colleciton: this) => boolean): TItem | undefined; + find(callback: (this: TContext, item: TItem, index: number, colleciton: this) => boolean, thisArg: TContext): TItem | undefined; + find(callback: (item: TItem, index: number, colleciton: this) => item is TResult): TResult | undefined; + find(callback: (this: TContext, item: TItem, index: number, colleciton: this) => item is TResult, thisArg: TContext): TResult | undefined; + + findLast(callback: (item: TItem, index: number, colleciton: this) => boolean): TItem | undefined; + findLast(callback: (this: TContext, item: TItem, index: number, colleciton: this) => boolean, thisArg: TContext): TItem | undefined; + findLast(callback: (item: TItem, index: number, colleciton: this) => item is TResult): TResult | undefined; + findLast(callback: (this: TContext, item: TItem, index: number, colleciton: this) => item is TResult, thisArg: TContext): TResult | undefined; + + concat(...items: readonly (TItem | readonly TItem[])[]): TItem[]; + + map(callback: (item: TItem, index: number, colleciton: this) => TResult): TResult[]; + map(callback: (this: TContext, item: TItem, index: number, colleciton: this) => TResult, thisArg: TContext): TResult[]; + + filter(callback: (item: TItem, index: number, collection: this) => boolean): TItem[]; + filter(callback: (this: TContext, item: TItem, index: number, collection: this) => boolean, thisArg: TContext): TItem[]; + filter(callback: (item: TItem, index: number, collection: this) => item is TResult): TResult[]; + filter(callback: (this: TContext, item: TItem, index: number, collection: this) => item is TResult, thisArg: TContext): TResult[]; + + slice(start?: number, end?: number): TItem[]; + + join(separator?: string): string; + + some(callback: (item: TItem, index: number, collection: this) => boolean): boolean; + some(callback: (this: TContext, item: TItem, index: number, collection: this) => boolean, thisArg: TContext): boolean; + + every(callback: (item: TItem, index: number, collection: this) => boolean): boolean; + every(callback: (this: TContext, item: TItem, index: number, collection: this) => boolean, thisArg: TContext): boolean; + + reduce(callback: (accumulator: TItem, item: TItem, index: number, colleciton: this) => TItem): TItem; + reduce(callback: (accumulator: TResult, item: TItem, index: number, colleciton: this) => TItem, initialValue: TResult): TItem; + + reduceRight(callback: (accumulator: TItem, item: TItem, index: number, colleciton: this) => TItem): TItem; + reduceRight(callback: (accumulator: TResult, item: TItem, index: number, colleciton: this) => TItem, initialValue: TResult): TItem; + + /** + * Converts the observable collection to a native JavaScript {@link Array}. * @returns An {@link Array} containing all the items in the collection. */ toArray(): TItem[]; + + toSorted(): TItem[]; + toSorted(compareCallback: (left: TItem, right: TItem) => number): TItem[]; + + toSpliced(start: number): TItem[]; + toSpliced(start: number, deleteCount: number): TItem[]; + toSpliced(start: number, deleteCount: number, ...newItems: readonly TItem[]): TItem[]; } /** Represents an observable collection based on the array interface. @@ -66,6 +139,14 @@ export interface IObservableCollection extends IReadOnlyObservableCollect */ splice(start: number, deleteCount?: number, ...items: readonly TItem[]): TItem[]; + sort(): this; + sort(compareCallback: (left: TItem, right: TItem) => number): this; + + reverse(): this; + + copyWithin(target: number, start: number): this; + copyWithin(target: number, start: number, end: number): this; + /** Clears the contents of the collection and returns the removed items, similar to calling `collection.splice(0)`. * @returns Returns the items that used to be in the collection. * @see [Array.splice](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) @@ -80,6 +161,10 @@ export interface IObservableCollection extends IReadOnlyObservableCollect * @see [Array.splice](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) */ reset(...items: readonly TItem[]): number; + + fill(item: TItem): this; + fill(item: TItem, start: number): this; + fill(item: TItem, start: number, end: number): this; } /** Represents a read-only observable collection which can be used as a base class for custom observable collections as well. @@ -87,52 +172,53 @@ export interface IObservableCollection extends IReadOnlyObservableCollect * @see [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) */ export class ReadOnlyObservableCollection extends ViewModel implements IReadOnlyObservableCollection { - private readonly _items: TItem[] = []; - private readonly _itemCleanupCallbacks: ItemRemovedCallback[][]; - private readonly _itemAdded: EventDispatcher>; - private readonly _itemRemoved: EventDispatcher>; - private readonly _collectionChanged: EventDispatcher>; + private _length: number; + private _changeToken: unknown; + private readonly _collectionChangedEvent: EventDispatcher>; /** Initializes a new instance of the {@link ReadOnlyObservableCollection} class. * @param items The items to initialize the collection with. */ public constructor(...items: readonly TItem[]) { super(); - this._items = [...items]; - this._items.forEach((item, index) => (this as any)[index] = item); - this._itemCleanupCallbacks = items.map(() => []); - this.itemAdded = this._itemAdded = new EventDispatcher>(); - this.itemRemoved = this._itemRemoved = new EventDispatcher>(); - this.collectionChanged = this._collectionChanged = new EventDispatcher>(); + for (let index = 0; index < items.length; index++) + Object.defineProperty(this, index, { + configurable: true, + enumerable: true, + value: items[index], + writable: false + }); + this._length = items.length; + this._changeToken = {}; + + this.collectionChanged = this._collectionChangedEvent = new EventDispatcher>(); } - /** Gets the item at the provided {@link n} index - * @param n The index from which to retrieve an item. - * @throws {@link RangeError} when the index is outside the bounds of the collection. + /** + * Gets the item at the provided {@link index} index + * @param index The index from which to retrieve an item. */ - readonly [n: number]: TItem; - - /** An event that is raised when an item is added to the collection. */ - public readonly itemAdded: IEvent>; - - /** An event that is raised when an item is removed from the collection. */ - public readonly itemRemoved: IEvent>; + readonly [index: number]: TItem; /** An event that is raised when the collection changed. */ public readonly collectionChanged: ICollectionChangedEvent; /** Gets the number of items in the collection. */ public get length(): number { - return this._items.length; + return this._length; } - /** Gets the item at the provided index. + /** + * Gets the item at the provided index. * @param index The index from which to retrieve an item. * @returns The item at the provided index. * @throws {@link RangeError} when the index is outside the bounds of the collection. */ public at(index: number): TItem { - return this._items[index]; + if (index < 0 || index >= this.length) + throw new RangeError(`The provided index is outside the bounds of the collection.`); + + return this[index]; } /** @@ -141,16 +227,8 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns a new {@link Array} containing the items of this collection followed by the items in the provided {@link Array}. * @see [Array.concat](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/concat) */ - public concat(...items: readonly TItem[]): TItem[] - /** - * Merges the current collection with the given {@link Array}s and returns a new JavaScript {@link Array}. - * @param items The item arrays to concatenate. - * @returns Returns a new {@link Array} containing the items of this collection followed by the items in the provided {@link Array}s. - * @see [Array.concat](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/concat) - */ - public concat(...items: readonly ConcatArray[]): TItem[] - public concat(...items: readonly (TItem | ConcatArray)[]): TItem[] { - return this._items.concat(...items); + public concat(...items: readonly (TItem | readonly TItem[])[]): TItem[] { + return this.toArray().concat(...items); } /** Aggregates the contained items into a {@link String} placing the provided `separator` between them. @@ -159,7 +237,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.join](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/join) */ public join(separator?: string): string { - return this._items.join(separator); + return this.toArray().join(separator); } /** @@ -170,7 +248,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.slice](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) */ public slice(start?: number, end?: number): TItem[] { - return this._items.slice(start, end); + return this.toArray().slice(start, end); } /** @@ -181,7 +259,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) */ public indexOf(searchElement: TItem, fromIndex?: number): number { - return this._items.indexOf(searchElement, fromIndex); + return this.toArray().indexOf(searchElement, fromIndex); } /** @@ -192,16 +270,9 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.lastIndexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf) */ public lastIndexOf(searchElement: TItem, fromIndex?: number): number { - return this._items.lastIndexOf(searchElement, fromIndex); + return this.toArray().lastIndexOf(searchElement, fromIndex); } - /** - * Checks whether all elements in the collection fulfil a given condition. - * @param predicate The callback performing the check for each item. - * @returns Returns `true` if the provided `predicate` is `true` for all items; otherwise `false`. - * @see [Array.every](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/every) - */ - public every(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean): boolean; /** * Checks whether all elements in the collection fulfil a given condition. * @template TContext The context type in which the callback is executed. @@ -210,19 +281,10 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns `true` if the provided `predicate` is `true` for all items; otherwise `false`. * @see [Array.every](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/every) */ - public every(predicate: (this: TContext, item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg: TContext): boolean; - public every(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg?: any): boolean { - const collection = this; - return this._items.every(function (this: any, item, index) { return predicate.call(this, item, index, collection); }, thisArg); + public every(predicate: (this: TContext, item: TItem, index: number, collection: this) => boolean, thisArg?: TContext): boolean { + return this.toArray().every((item, index) => predicate.call(thisArg, item, index, this)); } - /** - * Checks whether some elements in the collection fulfil a given condition. - * @param predicate The callback performing the check for each item. - * @returns Returns `true` if the provided `predicate` is `true` for at least one item; otherwise `false`. - * @see [Array.some](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/some) - */ - public some(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean): boolean; /** * Checks whether some elements in the collection fulfil a given condition. * @template TContext The context type in which the callback is executed. @@ -231,18 +293,10 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns `true` if the provided `predicate` is `true` for at least one item; otherwise `false`. * @see [Array.some](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/some) */ - public some(predicate: (this: TContext, item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg: TContext): boolean; - public some(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg?: any): boolean { - const collection = this; - return this._items.some(function (this: any, item, index) { return predicate.call(this, item, index, collection); }, thisArg); + public some(predicate: (this: TContext, item: TItem, index: number, collection: this) => boolean, thisArg?: TContext): boolean { + return this.toArray().some((item, index) => predicate.call(thisArg, item, index, this)); } - /** - * Executes the given callback for each item in the collection. - * @param callbackfn The callback processing each item. - * @see [Array.forEach](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) - */ - public forEach(callbackfn: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => void): void; /** * Executes the given callback for each item in the collection. * @template TContext The context type in which the callback is executed. @@ -250,12 +304,11 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @param thisArg A value to use as context when processing items. * @see [Array.forEach](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) */ - public forEach(callbackfn: (this: TContext, item: TItem, index: number, collection: IReadOnlyObservableCollection) => void, thisArg: TContext): void; - public forEach(callbackfn: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => void, thisArg?: any): void { - const collection = this; - return this._items.forEach(function (this: any, item, index) { callbackfn.call(this, item, index, collection); }, thisArg); + public forEach(callbackfn: (this: TContext, item: TItem, index: number, collection: this) => void, thisArg?: TContext): void { + this.toArray().forEach((item, index) => { + callbackfn.call(thisArg, item, index, this); + }); } - /** * Creates a new JavaScript {@link Array} constructed by mapping each item in the collection using a callback. * @template TResult The type to map each item to. @@ -263,7 +316,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns A new {@link Array} containing the mapped items. * @see [Array.map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/map) */ - public map(callbackfn: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => TResult): TResult[]; + public map(callbackfn: (item: TItem, index: number, collection: this) => TResult): TResult[]; /** * Creates a new JavaScript {@link Array} constructed by mapping each item in the collection using a callback. * @template TResult The type to map each item to. @@ -273,19 +326,11 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns A new {@link Array} containing the mapped items. * @see [Array.map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/map) */ - public map(callbackfn: (this: TContext, item: TItem, index: number, collection: IReadOnlyObservableCollection) => TResult, thisArg: TContext): TResult[]; - public map(callbackfn: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => TResult, thisArg?: any): TResult[] { - const collection = this; - return this._items.map(function (this: any, item, index) { return callbackfn.call(this, item, index, collection); }, thisArg); + public map(callbackfn: (this: TContext, item: TItem, index: number, collection: this) => TResult, thisArg: TContext): TResult[]; + public map(callbackfn: (this: TContext, item: TItem, index: number, collection: this) => TResult, thisArg?: TContext): TResult[] { + return this.toArray().map((item, index) => callbackfn.call(thisArg, item, index, this)); } - /** - * Creates a new JavaScript {@link Array} containing only the items for which the provided `predicate` evaluates to `true`. - * @param predicate The callback indicating which items to add in the result {@link Array}. - * @returns A new {@link Array} containing the items for which the provided `predicate` evaluated to `true`. - * @see [Array.filter](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) - */ - public filter(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean): TItem[]; /** * Creates a new JavaScript {@link Array} containing only the items for which the provided `predicate` evaluates to `true`. * @template TContext The context type in which the callback is executed. @@ -294,15 +339,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns A new {@link Array} containing the items for which the provided `predicate` evaluated to `true`. * @see [Array.filter](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) */ - public filter(predicate: (this: TContext, item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg: TContext): TItem[]; - /** - * Creates a new JavaScript {@link Array} containing only the items for which the provided `predicate` evaluates to `true`. - * @template TResult The type to convert each item to. - * @param predicate The callback indicating which items to add in the result {@link Array}. - * @returns A new {@link Array} containing the items for which the provided `predicate` evaluated to `true`. - * @see [Array.filter](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) - */ - public filter(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => item is TResult): TResult[]; + public filter(predicate: (this: TContext, item: TItem, index: number, collection: this) => boolean, thisArg?: TContext): TItem[]; /** * Creates a new JavaScript {@link Array} containing only the items for which the provided `predicate` evaluates to `true`. * @template TResult The type to convert each item to. @@ -312,10 +349,9 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns A new {@link Array} containing the items for which the provided `predicate` evaluated to `true`. * @see [Array.filter](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) */ - public filter(predicate: (this: TContext, item: TItem, index: number, collection: IReadOnlyObservableCollection) => item is TResult, thisArg: TContext): TResult[]; - public filter(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => item is TResult, thisArg?: any): TResult[] { - const collection = this; - return this._items.filter(function (this: any, item, index): item is TResult { return predicate.call(this, item, index, collection); }, thisArg); + public filter(predicate: (this: TContext, item: TItem, index: number, collection: this) => item is TResult, thisArg?: TContext): TResult[]; + public filter(predicate: (this: TContext, item: TItem, index: number, collection: this) => boolean, thisArg?: TContext): TResult[] { + return this.toArray().filter((item, index) => predicate.call(thisArg, item, index, this)) as TResult[]; } /** @@ -324,7 +360,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns a single aggregated item. * @see [Array.reduce](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) */ - public reduce(callbackfn: (previousValue: TItem, currentItem: TItem, currentIndex: number, collection: IReadOnlyObservableCollection) => TItem): TItem; + public reduce(callbackfn: (previousValue: TItem, currentItem: TItem, currentIndex: number, collection: this) => TItem): TItem; /** * Aggregates the collection to a single value. * @template TResult The result value type to which items are aggregated. @@ -333,10 +369,12 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns the value containing the aggregated collection. * @see [Array.reduce](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) */ - public reduce(callbackfn: (previousValue: TResult, currentItem: TItem, currentIndex: number, collection: IReadOnlyObservableCollection) => TResult, initialValue: TResult): TResult; + public reduce(callbackfn: (previousValue: TResult, currentItem: TItem, currentIndex: number, collection: this) => TResult, initialValue: TResult): TResult; public reduce(callbackfn: any, initialValue?: any): any { - const collection = this; - return this._items.reduce(function (this: any, previousValue, currentItem, currentIndex) { return callbackfn.call(this, previousValue, currentItem, currentIndex, collection); }, initialValue); + return this.toArray().reduce( + (previousValue, currentItem, currentIndex) => callbackfn(previousValue, currentItem, currentIndex, this), + initialValue + ); } /** @@ -345,7 +383,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns a single aggregated item. * @see [Array.reduceRight](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight) */ - public reduceRight(callbackfn: (previousValue: TItem, currentItem: TItem, currentIndex: number, collection: IReadOnlyObservableCollection) => TItem): TItem; + public reduceRight(callbackfn: (previousValue: TItem, currentItem: TItem, currentIndex: number, collection: this) => TItem): TItem; /** * Aggregates the collection to a single value by iterating the collection from end to start. * @template TResult The result value type to which items are aggregated. @@ -354,20 +392,14 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns the value containing the aggregated collection. * @see [Array.reduceRight](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight) */ - public reduceRight(callbackfn: (previousValue: TResult, currentItem: TItem, currentIndex: number, collection: IReadOnlyObservableCollection) => TResult, initialValue: TResult): TResult; + public reduceRight(callbackfn: (previousValue: TResult, currentItem: TItem, currentIndex: number, collection: this) => TResult, initialValue: TResult): TResult; public reduceRight(callbackfn: any, initialValue?: any): any { - const collection = this; - return this._items.reduceRight(function (this: any, previousValue, currentItem, currentIndex) { return callbackfn.call(this, collection, previousValue, currentItem, currentIndex, collection); }, initialValue); + return this.toArray().reduceRight( + (previousValue, currentItem, currentIndex) => callbackfn(previousValue, currentItem, currentIndex, this), + initialValue + ); } - /** - * Returns the first item for which the provided `predicate` evaluated to `true`, if no item can be found then `undefined` is returned. - * @template TResult The type of item to return. - * @param predicate The callback performing the item check. - * @returns Returns the first item for which the provided `predicate` evaluates to `true`; otherwise `undefined`. - * @see [Array.find](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/find) - */ - public find(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean): TItem | undefined; /** * Returns the first item for which the provided `predicate` evaluated to `true`, if no item can be found then `undefined` is returned. * @template TResult The type of item to return. @@ -377,15 +409,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns the first item for which the provided `predicate` evaluates to `true`; otherwise `undefined`. * @see [Array.find](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/find) */ - public find(predicate: (this: TContext, item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg: TContext): TItem | undefined; - /** - * Returns the first item for which the provided `predicate` evaluated to `true`, if no item can be found then `undefined` is returned. - * @template TResult The type of item to return. - * @param predicate The callback performing the item check. - * @returns Returns the first item for which the provided `predicate` evaluates to `true`; otherwise `undefined`. - * @see [Array.find](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/find) - */ - public find(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => item is TResult): TResult | undefined; + public find(predicate: (this: TContext, item: TItem, index: number, collection: this) => boolean, thisArg?: TContext): TItem | undefined; /** * Returns the first item for which the provided `predicate` evaluated to `true`, if no item can be found then `undefined` is returned. * @template TResult The type of item to return. @@ -395,38 +419,21 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns the first item for which the provided `predicate` evaluates to `true`; otherwise `undefined`. * @see [Array.find](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/find) */ - public find(predicate: (this: TContext, item: TItem, index: number, collection: IReadOnlyObservableCollection) => item is TResult, thisArg: TContext): TResult | undefined; - public find(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg?: any): TItem | undefined { - const collection = this; - return this._items.find(function (this: any, item, index): item is any { return predicate.call(this, item, index, collection); }, thisArg); + public find(predicate: (this: TContext, item: TItem, index: number, collection: this) => item is TResult, thisArg?: TContext): TResult | undefined; + public find(predicate: (item: TItem, index: number, collection: this) => boolean, thisArg?: TContext): TResult | undefined { + return this.toArray().find((item, index) => predicate.call(thisArg, item, index, this)) as TResult | undefined; } /** * Returns the last item for which the provided `predicate` evaluated to `true`, if no item can be found then `undefined` is returned. * @template TResult The type of item to return. - * @param predicate The callback performing the item check. - * @returns Returns the last item for which the provided `predicate` evaluates to `true`; otherwise `undefined`. - * @see [Array.findLast](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast) - */ - public findLast(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean): TItem | undefined; - /** - * Returns the last item for which the provided `predicate` evaluated to `true`, if no item can be found then `undefined` is returned. - * @template TResult The type of item to return. - * @template TContext The context type in which the callback is executed. + * @template TContext The context type in which the callback is executed. * @param predicate The callback performing the item check. * @param thisArg A value to use as context when evaluating items. * @returns Returns the last item for which the provided `predicate` evaluates to `true`; otherwise `undefined`. * @see [Array.findLast](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast) */ - public findLast(predicate: (this: TContext, item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg: TContext): TItem | undefined; - /** - * Returns the last item for which the provided `predicate` evaluated to `true`, if no item can be found then `undefined` is returned. - * @template TResult The type of item to return. - * @param predicate The callback performing the item check. - * @returns Returns the last item for which the provided `predicate` evaluates to `true`; otherwise `undefined`. - * @see [Array.findLast](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast) - */ - public findLast(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => item is TResult): TResult | undefined; + public findLast(predicate: (this: TContext, item: TItem, index: number, collection: this) => boolean, thisArg?: TContext): TItem | undefined; /** * Returns the last item for which the provided `predicate` evaluated to `true`, if no item can be found then `undefined` is returned. * @template TResult The type of item to return. @@ -436,19 +443,11 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns the last item for which the provided `predicate` evaluates to `true`; otherwise `undefined`. * @see [Array.findLast](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/findLast) */ - public findLast(predicate: (this: TContext, item: TItem, index: number, collection: IReadOnlyObservableCollection) => item is TResult, thisArg: TContext): TResult | undefined; - public findLast(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg?: any): TItem | undefined { - const collection = this; - return this._items.findLast(function (this: any, item, index): item is any { return predicate.call(this, item, index, collection); }, thisArg); + public findLast(predicate: (this: TContext, item: TItem, index: number, collection: this) => item is TResult, thisArg?: TContext): TResult | undefined; + public findLast(predicate: (item: TItem, index: number, collection: this) => boolean, thisArg?: TContext): TResult | undefined { + return this.toArray().findLast((item, index) => predicate.call(thisArg, item, index, this)) as TResult | undefined; } - /** - * Returns the index of the first item for which the provided `predicate` evaluated to `true`, if no item can be found then `-1` is returned. - * @param predicate The callback performing the item check. - * @returns Returns the index of the first item for which the provided `predicate` evaluates to `true`; otherwise `-1`. - * @see [Array.findIndex](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex) - */ - public findIndex(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean): number; /** * Returns the index of the first item for which the provided `predicate` evaluated to `true`, if no item can be found then `-1` is returned. * @template TContext The context type in which the callback is executed. @@ -457,19 +456,10 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns the index of the first item for which the provided `predicate` evaluates to `true`; otherwise `-1`. * @see [Array.findIndex](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex) */ - public findIndex(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg: TContext): number; - public findIndex(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg?: any): number { - const collection = this; - return this._items.findIndex(function (this: any, item, index) { return predicate.call(this, item, index, collection); }, thisArg); + public findIndex(predicate: (item: TItem, index: number, collection: this) => boolean, thisArg?: TContext): number { + return this.toArray().findIndex((item, index) => predicate.call(thisArg, item, index, this)); } - /** - * Returns the index of the last item for which the provided `predicate` evaluated to `true`, if no item can be found then `-1` is returned. - * @param predicate The callback performing the item check. - * @returns Returns the index of the last item for which the provided `predicate` evaluates to `true`; otherwise `-1`. - * @see [Array.findLastIndex](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex) - */ - public findLastIndex(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean): number; /** * Returns the index of the last item for which the provided `predicate` evaluated to `true`, if no item can be found then `-1` is returned. * @template TContext The context type in which the callback is executed. @@ -478,10 +468,8 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @returns Returns the index of the last item for which the provided `predicate` evaluates to `true`; otherwise `-1`. * @see [Array.findLastIndex](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex) */ - public findLastIndex(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg: TContext): number; - public findLastIndex(predicate: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => boolean, thisArg?: any): number { - const collection = this; - return this._items.findLastIndex(function (this: any, item, index) { return predicate.call(this, item, index, collection); }, thisArg); + public findLastIndex(predicate: (item: TItem, index: number, collection: this) => boolean, thisArg?: TContext): number { + return this.toArray().findLastIndex((item, index) => predicate.call(thisArg, item, index, this)); } /** @@ -490,7 +478,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.entries](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/entries) */ public entries(): IterableIterator<[number, TItem]> { - return this._items.entries(); + return this.toArray().entries(); } /** @@ -499,7 +487,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.keys](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/keys) */ public keys(): IterableIterator { - return this._items.keys(); + return this.toArray().keys(); } /** @@ -508,7 +496,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.values](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/values) */ public values(): IterableIterator { - return this._items.values(); + return this.toArray().values(); } /** @@ -519,57 +507,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.includes](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/includes) */ public includes(searchElement: TItem, fromIndex?: number): boolean { - return this._items.includes(searchElement, fromIndex); - } - - /** - * Returns a new JavaScript {@link Array} by mapping all items in the collection and the flattening the result by one level. - * @template TResult The type to map each item to. - * @param callback The callback mapping each item. - * @returns Returns a mapped and one level flattened {@link Array}. - * @see [Array.flatMap](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap) - */ - public flatMap(callback: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => TResult | readonly TResult[]): TResult[]; - /** - * Returns a new JavaScript {@link Array} by mapping all items in the collection and the flattening the result by one level. - * @template TResult The type to map each item to. - * @template TContext The context type in which the callback is executed. - * @param callback The callback mapping each item. - * @param thisArg A value to use as context when evaluating items. - * @returns Returns a mapped and one level flattened {@link Array}. - * @see [Array.flatMap](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap) - */ - public flatMap(callback: (item: TItem, index: number, collection: IReadOnlyObservableCollection) => TResult | readonly TResult[], thisArg: TContext): TResult[]; - /** - * Returns a new JavaScript {@link Array} by mapping all items in the collection and the flattening the result by one level. - * @template TResult The type to map each item to. - * @param callback The callback mapping each item. - * @param thisArg A value to use as context when evaluating items. - * @returns Returns a mapped and one level flattened {@link Array}. - * @see [Array.flatMap](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap) - */ - public flatMap(callback: (item: TItem, index: number, collection: any) => TResult | readonly TResult[], thisArg?: any): TResult[]; - public flatMap(callback: (item: TItem, index: number, collection: any) => TResult | readonly TResult[], thisArg?: any): TResult[] { - const collection = this; - return this._items.flatMap(function (this: any, item, index) { return callback.call(this, item, index, collection); }, thisArg); - } - - /** - * Returns a new JavaScript {@link Array} containing the flattened sub-arrays by recursively concatenating them. - * @param depth The number of levels to flatten. - * @returns Returns a flattened {@link Array}. - * @see [Array.flat](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/flat) - */ - public flat(this: TContext, depth?: TDepth): FlatArray[]; - /** - * Returns a new JavaScript {@link Array} containing the flattened sub-arrays by recursively concatenating them. - * @template TDepth The number of levels to flatten. - * @param depth The number of levels to flatten. - * @returns Returns a flattened {@link Array}. - * @see [Array.flat](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/flat) - */ - public flat(depth?: TDepth): FlatArray[] { - return this._items.flat.call(this._items, depth); + return this.toArray().includes(searchElement, fromIndex); } /** Returns a JavaScript [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) containing the items of the collection in reverse order. @@ -577,7 +515,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.toReversed](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/toReversed) */ public toReversed(): TItem[] { - return [...this._items].reverse(); + return this.toArray().reverse(); } /** Returns a JavaScript [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) containing the items of the collection in ascending order. @@ -586,9 +524,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.toSorted](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted) */ public toSorted(compareFn?: (a: TItem, b: TItem) => number): TItem[] { - const result = [...this._items]; - result.sort(compareFn); - return result; + return this.toArray().sort(compareFn); } /** Returns a JavaScript [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) containing the spliced items of the collection. @@ -600,9 +536,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.toSpliced](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced) */ public toSpliced(start: number, deleteCount?: number, ...items: readonly TItem[]): TItem[] { - const result = [...this._items]; - result.splice(start, deleteCount, ...items); - return result; + return this.toArray().splice(start, deleteCount, ...items); } /** Returns a JavaScript [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) containing the elements from the collection and having the one at the provided index replaced with the provided value. @@ -612,7 +546,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.with](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/with) */ public with(index: number, value: TItem): TItem[] { - const result = [...this._items]; + const result = this.toArray(); result[index] = value; return result; } @@ -623,38 +557,16 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array@iterator](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/@@iterator) */ public [Symbol.iterator](): IterableIterator { - return this._items[Symbol.iterator](); + const changeTokenCopy = this._changeToken; + return new ObservableCollectionIterator(this, () => changeTokenCopy !== this._changeToken); } /** - * Returns an object specifying how properties are handled inside a `with` statement. - * @returns Returns an object specifying how properties are handled inside a `with` statement. - * @see [Array@unscopables](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/@@unscopables) - */ - public readonly [Symbol.unscopables]: any = { - _items: true, - _itemCleanupCallbacks: true, - _itemAdded: true, - _itemRemoved: true, - _collectionChanged: true, - at: true, - entries: true, - find: true, - findIndex: true, - findLast: true, - findLastIndex: true, - flat: true, - flatMap: true, - includes: true, - keys: true, - values: true - }; - - /** Converts the observable collection to a native JavaScript {@link Array}. + * Converts the observable collection to a native JavaScript {@link Array}. * @returns An {@link Array} containing all the items in the collection. */ public toArray(): TItem[] { - return [...this._items]; + return Array.from(this); } /** @@ -664,8 +576,34 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.push](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/push) */ protected push(...items: readonly TItem[]): number { - this._splice(this.length, 0, items); - return this.length; + if (items.length > 0) { + this._changeToken = {}; + + const startIndex = this._length; + const addedIndexes: number[] = []; + + for (let index = 0; index < items.length; index++) { + const newIndex = startIndex + index; + addedIndexes.push(newIndex); + Object.defineProperty(this, startIndex + index, { + configurable: true, + enumerable: true, + value: items[index], + writable: false + }); + } + this._length += items.length; + + this._collectionChangedEvent.dispatch(this, { + operation: "push", + startIndex, + addedItems: items, + removedItems: [] + }); + this.notifyPropertiesChanged("length", ...addedIndexes); + } + + return this._length; } /** Removes the last element from the collection and returns it. If the collection is empty, `undefined` is returned. @@ -673,8 +611,27 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @see [Array.pop](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/pop) */ protected pop(): TItem | undefined { - const removedItems = this._splice(this.length - 1, 1, []); - return removedItems.length > 0 ? removedItems[0] : undefined; + if (this._length === 0) + return undefined; + else { + this._changeToken = {}; + + const removedIndex = this._length - 1; + const removedItem = this[removedIndex]; + + this._length--; + delete (this as Record)[removedIndex]; + + this._collectionChangedEvent.dispatch(this, { + operation: "pop", + startIndex: removedIndex, + addedItems: [], + removedItems: [removedItem] + }); + this.notifyPropertiesChanged("length", removedIndex); + + return removedItem; + } } /** Inserts new elements at the start of the collection, and returns the new length of the collection. @@ -693,7 +650,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR */ protected shift(): TItem | undefined { const removedItems = this._splice(0, 1, []); - return removedItems.length > 0 ? removedItems[0] : undefined; + return removedItems[0]; } /** Gets the item at the provided index. @@ -702,7 +659,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @throws {@link RangeError} when the index is outside the bounds of the collection. */ protected get(index: number): TItem { - if (index < 0 || index >= this._items.length) + if (index < 0 || index >= this.length) throw new RangeError('The index is outside the bounds of the collection.'); return this[index]; @@ -714,7 +671,7 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR * @throws {@link RangeError} when the index is outside the bounds of the collection. */ protected set(index: number, item: TItem): void { - if (index < 0 || index >= this._items.length) + if (index < 0 || index >= this.length) throw new RangeError('The index is outside the bounds of the collection.'); this._splice(index, 1, [item]); @@ -731,6 +688,29 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR return this._splice(start, deleteCount, items); } + protected sort(): this; + protected sort(compareCallback: (left: TItem, right: TItem) => number): this; + protected sort(compareCallback?: (left: TItem, right: TItem) => number): this { + throw new Error('Method not implemented.'); + } + + protected reverse(): this { + throw new Error('Method not implemented.'); + } + + protected copyWithin(target: number, start: number): this; + protected copyWithin(target: number, start: number, end: number): this; + protected copyWithin(target: number, start: number, end?: number): this { + throw new Error('Method not implemented.'); + } + + protected fill(item: TItem): this; + protected fill(item: TItem, start: number): this; + protected fill(item: TItem, start: number, end: number): this; + protected fill(item: TItem, start?: number, end?: number): this { + throw new Error('Method not implemented.'); + } + /** Clears the contents of the collection and returns the removed items, similar to calling `collection.splice(0)`. * @returns Returns the items that used to be in the collection. * @see [Array.splice](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) @@ -753,57 +733,47 @@ export class ReadOnlyObservableCollection extends ViewModel implements IR private _splice(start: number, deleteCount: number, items: readonly TItem[]): TItem[] { if (start < 0) - start = Math.max(0, this._items.length + start); - else if (start > this._items.length) - start = this._items.length; + start = Math.max(0, this.length + start); + else if (start > this.length) + start = this.length; if (deleteCount === undefined) - deleteCount = this._items.length - start; + deleteCount = this.length - start; else if (deleteCount < 0) deleteCount = 0; if (items === undefined) items = []; - const previousLength = this._items.length; + const previousLength = this.length; if (deleteCount < items.length) { const gap = items.length - deleteCount; - for (let index = this._items.length + gap; index > start + items.length; index--) + for (let index = this.length + gap; index > start + items.length; index--) (this as any)[index] = this[index - gap]; } else if (deleteCount > items.length) { const gap = deleteCount - items.length; - for (let index = start + items.length + gap; index < this._items.length; index++) + for (let index = start + items.length + gap; index < this.length; index++) (this as any)[index - gap] = this[index]; - for (let index = this._items.length - gap; index < this._items.length; index++) + for (let index = this.length - gap; index < this.length; index++) delete (this as any)[index]; } for (let index = 0; index < items.length; index++) (this as any)[index + start] = items[index]; - const removedItems = this._items.splice(start, deleteCount, ...items); - const removedItemsCallbacks = this._itemCleanupCallbacks.splice(start, deleteCount, ...items.map(() => [])); - removedItems.forEach((item, index) => { - removedItemsCallbacks[index].forEach(callback => callback(item, index + start)); - }); - - removedItems.forEach((item, index) => this._itemRemoved.dispatch(this, { - item, - index: index + start - })); - items.forEach((item, index) => this._itemAdded.dispatch(this, { - item, - index: index + start, - addItemRemovalCallback: callback => this._itemCleanupCallbacks[index + start].push(callback) - })); + const removedItems = this.splice(start, deleteCount, ...items); if (removedItems.length > 0 || items.length > 0) - this._collectionChanged.dispatch(this, { - addedItems: items.length > 0 ? new Array(start).concat(items) : items, - removedItems: removedItems.length > 0 ? new Array(start).concat(removedItems) : removedItems + this._collectionChangedEvent.dispatch(this, { + startIndex: start, + addedItems: items, + removedItems: removedItems, + get operation(): CollectionOperation { + throw Error("unknown"); + } }); - if (previousLength !== this._items.length) + if (previousLength !== this.length) this.notifyPropertiesChanged('length'); return removedItems; @@ -886,6 +856,29 @@ export class ObservableCollection extends ReadOnlyObservableCollection number): this; + public sort(compareCallback?: (left: TItem, right: TItem) => number): this { + return super.sort(compareCallback); + } + + public reverse(): this { + return super.reverse(); + } + + public copyWithin(target: number, start: number): this; + public copyWithin(target: number, start: number, end: number): this; + public copyWithin(target: number, start: number, end?: number): this { + return this.copyWithin(target, start, end); + } + + public fill(item: TItem): this; + public fill(item: TItem, start: number): this; + public fill(item: TItem, start: number, end: number): this; + public fill(item: TItem, start?: number, end?: number): this { + return super.fill(item, start, end); + } + /** Clears the contents of the collection and returns the removed items, similar to calling `collection.splice(0)`. * @returns Returns the items that used to be in the collection. * @see [Array.splice](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) @@ -933,4 +926,62 @@ export class ObservableCollection extends ReadOnlyObservableCollection implements Iterator { + private _completed: boolean; + private _index: number; + private readonly _observableCollection: ReadOnlyObservableCollection; + private readonly _collectionChanged: () => boolean; + + public constructor(observableCollection: ReadOnlyObservableCollection, collectionChanged: () => boolean) { + this._index = 0; + this._observableCollection = observableCollection; + this._completed = this._index >= this._observableCollection.length; + this._collectionChanged = collectionChanged; + } + + public [Symbol.iterator](): IterableIterator { + return this; + } + + public next(): IteratorResult { + if (this._completed) + return { + done: true, + value: undefined + }; + else if (this._collectionChanged()) + throw new Error("Collection has changed while being iterated."); + else { + const value = this._observableCollection[this._index]; + this._index++; + + this._completed = this._index >= this._observableCollection.length; + + return { + done: false, + value + }; + } + } + + public return(value?: TItem): IteratorResult { + this._completed = true; + + return { + done: true, + value + }; + } + + public throw(): IteratorResult { + this._completed = true; + + return { + done: true, + value: undefined + }; + } + } \ No newline at end of file diff --git a/src/validation.ts b/src/validation.ts index 0a7469d..84958a6 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -46,7 +46,7 @@ export type ValidatorCallback = (validatable: T) => string | undefined; * @param item The item from which the validatable has been selected. * @param collection The collection to which the item belongs. */ -export type CollectionItemValidatorCallback = (validatable: TValidatable, item: TItem, collection: readonly TItem[]) => string | undefined; +export type CollectionItemValidatorCallback = (validatable: TValidatable, item: TItem, collection: Iterable) => string | undefined; /** Represents a validatable selector callback. * @template TItem The type of items from which to select validatable objects. @@ -391,7 +391,7 @@ function applyValidators(validatable: TValida validatable.error = error; } -function applyCollectionItemValidators(validatable: TValidatable, item: TItem, collection: readonly TItem[], validators: readonly (CollectionItemValidatorCallback | undefined)[]): void { +function applyCollectionItemValidators(validatable: TValidatable, item: TItem, collection: Iterable, validators: readonly (CollectionItemValidatorCallback | undefined)[]): void { let index = 0; let error = undefined; while (index < validators.length && error === undefined) { diff --git a/tests/components/input-tests.tsx b/tests-old/components/input-tests.tsx similarity index 100% rename from tests/components/input-tests.tsx rename to tests-old/components/input-tests.tsx diff --git a/tests/events-tests.ts b/tests-old/events-tests.ts similarity index 100% rename from tests/events-tests.ts rename to tests-old/events-tests.ts diff --git a/tests/form-field-collection-view-model-tests.ts b/tests-old/form-field-collection-view-model-tests.ts similarity index 100% rename from tests/form-field-collection-view-model-tests.ts rename to tests-old/form-field-collection-view-model-tests.ts diff --git a/tests/form-field-view-model-tests.ts b/tests-old/form-field-view-model-tests.ts similarity index 100% rename from tests/form-field-view-model-tests.ts rename to tests-old/form-field-view-model-tests.ts diff --git a/tests/hooks/use-view-model-factory-tests.tsx b/tests-old/hooks/use-view-model-factory-tests.tsx similarity index 100% rename from tests/hooks/use-view-model-factory-tests.tsx rename to tests-old/hooks/use-view-model-factory-tests.tsx diff --git a/tests/hooks/use-view-model-type-tests.tsx b/tests-old/hooks/use-view-model-type-tests.tsx similarity index 100% rename from tests/hooks/use-view-model-type-tests.tsx rename to tests-old/hooks/use-view-model-type-tests.tsx diff --git a/tests/hooks/watch-collection-tests.tsx b/tests-old/hooks/watch-collection-tests.tsx similarity index 100% rename from tests/hooks/watch-collection-tests.tsx rename to tests-old/hooks/watch-collection-tests.tsx diff --git a/tests/hooks/watch-event-tests.tsx b/tests-old/hooks/watch-event-tests.tsx similarity index 100% rename from tests/hooks/watch-event-tests.tsx rename to tests-old/hooks/watch-event-tests.tsx diff --git a/tests/hooks/watch-view-model-tests.tsx b/tests-old/hooks/watch-view-model-tests.tsx similarity index 100% rename from tests/hooks/watch-view-model-tests.tsx rename to tests-old/hooks/watch-view-model-tests.tsx diff --git a/tests/observable-collection-tests.ts b/tests-old/observable-collection-tests.ts similarity index 100% rename from tests/observable-collection-tests.ts rename to tests-old/observable-collection-tests.ts diff --git a/tests/validation-tests.ts b/tests-old/validation-tests.ts similarity index 100% rename from tests/validation-tests.ts rename to tests-old/validation-tests.ts diff --git a/tests/view-model-tests.ts b/tests-old/view-model-tests.ts similarity index 100% rename from tests/view-model-tests.ts rename to tests-old/view-model-tests.ts diff --git a/tests/observable-collection/common/expectCollectionsToBeEqual.ts b/tests/observable-collection/common/expectCollectionsToBeEqual.ts new file mode 100644 index 0000000..b34326a --- /dev/null +++ b/tests/observable-collection/common/expectCollectionsToBeEqual.ts @@ -0,0 +1,36 @@ +import type { IReadOnlyObservableCollection } from "../../../src/observable-collection"; + +export function expectCollectionsToBeEqual(observableCollection: IReadOnlyObservableCollection, array: readonly TItem[]): void { + expect(observableCollection.length).toBe(array.length); + for (let index = 0; index < observableCollection.length; index++) { + expect(observableCollection[index]).toBe(array[index]); + expect(observableCollection.at(index)).toBe(array[index]); + } + expect(observableCollection.toArray()).toEqual(array); + + expectIndexesToBeDefined(observableCollection); + expectIterationsToBeEqual(observableCollection, array); +} + +function expectIndexesToBeDefined(observableCollection: IReadOnlyObservableCollection): void { + expect(-1 in observableCollection).toBe(false); + expect(observableCollection.length in observableCollection).toBe(false); + expect((observableCollection.length + 1) in observableCollection).toBe(false); + + for (let index = 0; index < observableCollection.length; index++) { + expect(index in observableCollection).toBe(true); + expect(observableCollection[index]).toStrictEqual(observableCollection.at(index)); + } +} + +function expectIterationsToBeEqual(observableCollection: IReadOnlyObservableCollection, array: readonly TItem[]): void { + const observableCollectionIterationResult: TItem[] = []; + for (const item of observableCollection) + observableCollectionIterationResult.push(item); + + const arrayIterationResult: TItem[] = []; + for (const item of array) + arrayIterationResult.push(item); + + expect(observableCollectionIterationResult).toEqual(arrayIterationResult); +} \ No newline at end of file diff --git a/tests/observable-collection/common/index.ts b/tests/observable-collection/common/index.ts new file mode 100644 index 0000000..bc9beab --- /dev/null +++ b/tests/observable-collection/common/index.ts @@ -0,0 +1,2 @@ +export { testMutatingOperation } from "./testMutatingOperation"; +export { testBlankMutatingOperation } from "./testBlankMutatingOperation"; \ No newline at end of file diff --git a/tests/observable-collection/common/testBlankMutatingOperation.ts b/tests/observable-collection/common/testBlankMutatingOperation.ts new file mode 100644 index 0000000..2882ae3 --- /dev/null +++ b/tests/observable-collection/common/testBlankMutatingOperation.ts @@ -0,0 +1,48 @@ +import type { ICollectionChangedEventHandler, IPropertiesChangedEventHandler } from "../../../src/events"; +import { type IObservableCollection, ObservableCollection } from "../../../src/observable-collection"; +import { expectCollectionsToBeEqual } from "./expectCollectionsToBeEqual"; + +/** + * Applies the callback to both an array and an observable collection constructed form the initial state, + * checking the two before and after the operation is applied as well as checking that the operation had + * no effect and no events were raised. + */ +export function testBlankMutatingOperation(applyOperation: (collection: TItem[] | IObservableCollection) => unknown, initialState: readonly TItem[]) { + let collectionChangedRaiseCount = 0; + const collectionChangedEventHandler: ICollectionChangedEventHandler, TItem> = { + handle() { + collectionChangedRaiseCount++; + } + } + let propertiesChangedRaiseCount = 0; + const propertiesChangedEventHandler: IPropertiesChangedEventHandler> = { + handle() { + propertiesChangedRaiseCount++; + } + } + + const arrayBeforeOperation = initialState.slice(); + const observableCollectionBeforeOperation = new ObservableCollection(...initialState); + observableCollectionBeforeOperation.collectionChanged.subscribe(collectionChangedEventHandler); + observableCollectionBeforeOperation.propertiesChanged.subscribe(propertiesChangedEventHandler); + + const arrayAfterOperation = initialState.slice(); + const observableCollectionAfterOperation = new ObservableCollection(...initialState); + observableCollectionAfterOperation.collectionChanged.subscribe(collectionChangedEventHandler); + observableCollectionAfterOperation.propertiesChanged.subscribe(propertiesChangedEventHandler); + + expectCollectionsToBeEqual(observableCollectionBeforeOperation, arrayBeforeOperation); + expectCollectionsToBeEqual(observableCollectionAfterOperation, arrayAfterOperation); + + const arrayResult = applyOperation(arrayAfterOperation); + const observableCollectionResult = applyOperation(observableCollectionAfterOperation); + + expect(collectionChangedRaiseCount).toBe(0); + expect(propertiesChangedRaiseCount).toBe(0); + + expectCollectionsToBeEqual(observableCollectionAfterOperation, arrayAfterOperation); + expect(observableCollectionResult).toBe(arrayResult); + + expect(arrayAfterOperation).toEqual(arrayBeforeOperation); + expect(observableCollectionAfterOperation).toEqual(observableCollectionBeforeOperation); +} \ No newline at end of file diff --git a/tests/observable-collection/common/testMutatingOperation.ts b/tests/observable-collection/common/testMutatingOperation.ts new file mode 100644 index 0000000..31fc262 --- /dev/null +++ b/tests/observable-collection/common/testMutatingOperation.ts @@ -0,0 +1,62 @@ +import type { CollectionOperation } from "../../../src/events"; +import { type IObservableCollection, ObservableCollection } from "../../../src/observable-collection"; +import { expectCollectionsToBeEqual } from "./expectCollectionsToBeEqual"; + +/** + * Applies the callback to both an array and an observable collection constructed form the initial state, + * checking the two before and after the operation is applied as well as checking the result of the operation. + * + * An observable collection provides all relevant methods that are exposed by a native Array thus the + * two need to behave the same. + * + * The approach is that any operation that is applied on an observable collection can be replicated using + * just the splice method through the collectionChanged event. + */ +export function testMutatingOperation(collectionOperation: CollectionOperation, applyOperation: (collection: TItem[] | IObservableCollection) => unknown, initialState: readonly TItem[]) { + let collectionChangedRaiseCount = 0; + let propertiesChangedRaiseCount = 0; + let expectedChangedProperties: readonly (keyof ObservableCollection)[] = []; + let actualChangedProperties: readonly (keyof ObservableCollection)[] = []; + + const array = initialState.slice(); + const observableCollection = new ObservableCollection(...initialState); + observableCollection.collectionChanged.subscribe({ + handle(subject, { operation, startIndex, addedItems, removedItems }) { + collectionChangedRaiseCount++; + + expect(subject).toStrictEqual(observableCollection); + expect(operation).toEqual(collectionOperation); + + const spliceArray = initialState.slice(); + const spliceRemovedItems = spliceArray.splice(startIndex, removedItems.length, ...addedItems); + + expectCollectionsToBeEqual(observableCollection, spliceArray); + expect(spliceRemovedItems).toEqual(removedItems); + + const changedProperties: (keyof ObservableCollection)[] = ["length"]; + for (let index = 0; index < Math.max(addedItems.length, removedItems.length); index++) + changedProperties.push(index + startIndex); + expectedChangedProperties = changedProperties; + } + }); + observableCollection.propertiesChanged.subscribe({ + handle(subject, changedProperties) { + propertiesChangedRaiseCount++; + + expect(subject).toStrictEqual(observableCollection); + actualChangedProperties = changedProperties; + } + }); + + expectCollectionsToBeEqual(observableCollection, array); + + const arrayResult = applyOperation(array); + const observableCollectionResult = applyOperation(observableCollection); + + expect(collectionChangedRaiseCount).toBe(1); + expect(propertiesChangedRaiseCount).toBe(1); + expect(actualChangedProperties).toEqual(expectedChangedProperties); + + expectCollectionsToBeEqual(observableCollection, array); + expect(observableCollectionResult).toBe(arrayResult); +} \ No newline at end of file diff --git a/tests/observable-collection/observable-collection.pop.tests.ts b/tests/observable-collection/observable-collection.pop.tests.ts new file mode 100644 index 0000000..0abb504 --- /dev/null +++ b/tests/observable-collection/observable-collection.pop.tests.ts @@ -0,0 +1,37 @@ +import { ObservableCollection } from "../../src/observable-collection"; +import { testBlankMutatingOperation, testMutatingOperation } from "./common"; + +describe('ObserableCollection.pop', (): void => { + it('poping an item from an empty collection has no effect', (): void => { + testBlankMutatingOperation(collection => collection.pop(), []); + }); + + it('poping an item from a non-empty collection returns the last item', (): void => { + testMutatingOperation("pop", collection => collection.pop(), [1, 2, 3]); + }); + + it('popping items while iterating will break iterators', (): void => { + expect( + () => { + const observableCollection = new ObservableCollection(1, 2, 3); + + for (let _ of observableCollection) + observableCollection.pop(); + }) + .toThrow(new Error("Collection has changed while being iterated.")) + }); + + it('popping items from empty collection while iterating will not break iterators', (): void => { + expect( + () => { + const observableCollection = new ObservableCollection(); + const iterator = observableCollection[Symbol.iterator](); + + observableCollection.pop(); + + iterator.next(); + }) + .not + .toThrow() + }); +}); \ No newline at end of file diff --git a/tests/observable-collection/observable-collection.push.tests.ts b/tests/observable-collection/observable-collection.push.tests.ts new file mode 100644 index 0000000..2df6db2 --- /dev/null +++ b/tests/observable-collection/observable-collection.push.tests.ts @@ -0,0 +1,51 @@ +import { ObservableCollection } from "../../src/observable-collection"; +import { testBlankMutatingOperation, testMutatingOperation } from "./common"; + +describe('ObserableCollection.push', (): void => { + it('pushing an item adds it to the collection', (): void => { + testMutatingOperation("push", collection => collection.push(1), []); + }); + + it('pushing an item to non-empty collection adds them to the collection', (): void => { + testMutatingOperation("push", collection => collection.push(4), [1, 2, 3]); + }); + + it('pushing items adds them to the collection', (): void => { + testMutatingOperation("push", collection => collection.push(1, 2, 3), []); + }); + + it('pushing items to non-empty collection adds them to the collection', (): void => { + testMutatingOperation("push", collection => collection.push(4, 5, 6), [1, 2, 3]); + }); + + it('not pushing any items to the collection has no effect', (): void => { + testBlankMutatingOperation(collection => collection.push(), [1, 2, 3]); + }); + + it('not pushing any items to non-empty collection has no effect', (): void => { + testBlankMutatingOperation(collection => collection.push(), []); + }); + + it('pushing items while iterating will break iterators', (): void => { + expect( + () => { + const observableCollection = new ObservableCollection(1, 2, 3); + + for (let _ of observableCollection) + observableCollection.push(1); + }) + .toThrow(new Error("Collection has changed while being iterated.")) + }); + + it('not pushing items while iterating will not break iterators', (): void => { + expect( + () => { + const observableCollection = new ObservableCollection(1, 2, 3); + + for (let _ of observableCollection) + observableCollection.push(); + }) + .not + .toThrow() + }); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index bcaac6b..b6d5ab7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES5", + "target": "ES2015", "lib": [ "ES2023" ], @@ -19,7 +19,7 @@ "src/**/*" ], "exclude": [ - "tests", + "tests-old", "lib", "node_modules" ]