Skip to content

Commit

Permalink
feat: Add moveBefore and moveAfter for array-nature collection
Browse files Browse the repository at this point in the history
Summary:
* Add moveBefore and moveAfter API on IndexedCollection
* Additional documentations on SignalObserver

Test Plan:
* Verify unit tests have passed
* Create a collection with nature as Array
* Populate the collection with a sequence of items
* Call moveBefore or moveAfter API
* Verify the .items property reflect the movement
  • Loading branch information
Tangent Lin authored and tangentlin committed Mar 22, 2024
1 parent 6e91bfe commit 5cff501
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 7 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "indexed-collection",
"version": "1.8.0",
"version": "1.9.0",
"description": "A zero-dependency library of classes that make filtering, sorting and observing changes to arrays easier and more efficient.",
"license": "MIT",
"keywords": [
Expand Down
18 changes: 18 additions & 0 deletions src/collections/IndexedCollectionBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@ export abstract class IndexedCollectionBase<T>
return true;
}

/**
* Move item before the specified item
* @param item The item to move
* @param before
*/
moveBefore(item: T, before: T): void {
this._allItemList.moveBefore(item, before);
}

/**
* Move item after the specified item
* @param item The item to move
* @param after
*/
moveAfter(item: T, after: T): void {
this._allItemList.moveAfter(item, after);
}

get items(): readonly T[] {
return this._allItemList.output;
}
Expand Down
14 changes: 14 additions & 0 deletions src/core/internals/IInternalList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ export interface IInternalList<T> {
*/
update(newItem: T, oldItem: T): void;

/**
* Move the item before another item
* @param item
* @param before
*/
moveBefore(item: T, before: T): void;

/**
* Move the item after another item
* @param item
* @param after
*/
moveAfter(item: T, after: T): void;

readonly output: readonly T[];

readonly count: number;
Expand Down
34 changes: 32 additions & 2 deletions src/core/internals/InternalList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export class InternalList<T> implements IInternalList<T> {
}

remove(item: T): void {
const index: number = this.source.findIndex(listItem => listItem === item);
const index: number = this.source.findIndex(
(listItem) => listItem === item
);
if (index >= 0) {
this.source.splice(index, 1);
this.invalidate();
Expand All @@ -48,11 +50,39 @@ export class InternalList<T> implements IInternalList<T> {

update(newItem: T, oldItem: T): void {
const index: number = this.source.findIndex(
listItem => listItem === oldItem
(listItem) => listItem === oldItem
);
if (index >= 0) {
this.source[index] = newItem;
this.invalidate();
}
}

moveBefore(item: T, before: T): void {
const itemIndex: number = this.source.findIndex(
(listItem) => listItem === item
);
const beforeIndex: number = this.source.findIndex(
(listItem) => listItem === before
);
if (itemIndex >= 0 && beforeIndex >= 0) {
this.source.splice(itemIndex, 1);
this.source.splice(beforeIndex, 0, item);
this.invalidate();
}
}

moveAfter(item: T, after: T): void {
const itemIndex: number = this.source.findIndex(
(listItem) => listItem === item
);
const afterIndex: number = this.source.findIndex(
(listItem) => listItem === after
);
if (itemIndex >= 0 && afterIndex >= 0) {
this.source.splice(itemIndex, 1);
this.source.splice(afterIndex, 0, item);
this.invalidate();
}
}
}
10 changes: 10 additions & 0 deletions src/core/internals/InternalSetList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,14 @@ export class InternalSetList<T> implements IInternalList<T> {
this.invalidate();
}
}

moveBefore(_item: T, _before: T): void {
// There is no concept of order in a set
return;
}

moveAfter(_item: T, _after: T): void {
// There is no concept of order in a set
return;
}
}
4 changes: 4 additions & 0 deletions src/signals/Signal.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* Signal is a base class for all signals.
*
*/
export abstract class Signal {
protected constructor(
public readonly type: symbol,
Expand Down
25 changes: 21 additions & 4 deletions src/signals/SignalObserver.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { ISignalObserver, SignalHandler } from './ISignalObserver';
import { Signal, SignalType } from './Signal';

/**
* Signal observer is a class that can be used to observe signals
* It supports multiple observers for a single signal type and vice versa
*/
export class SignalObserver implements ISignalObserver {
private readonly typeToHandleMap: Map<SignalType, Set<SignalHandler<Signal>>>;
private handlerToTypeMap: Map<
SignalHandler<Signal>,
Set<SignalType>
> = new Map();
private handlerToTypeMap: Map<SignalHandler<Signal>, Set<SignalType>> =
new Map();
constructor() {
this.typeToHandleMap = new Map();
}

/**
* Notify all observers of a signal by the signal's type
* @param signal
*/
notifyObservers(signal: Signal): void {
const handlers = this.typeToHandleMap.get(signal.type);
if (handlers) {
Expand All @@ -19,6 +26,11 @@ export class SignalObserver implements ISignalObserver {
}
}

/**
* Register an observer for a signal type
* @param type The type of a signal
* @param handler The handler to be called when the signal is emitted
*/
registerObserver<T extends Signal>(
type: symbol,
handler: SignalHandler<T>
Expand All @@ -35,6 +47,11 @@ export class SignalObserver implements ISignalObserver {
this.handlerToTypeMap.set(handler, types);
}

/**
* Unregister an observer for a signal type
* @param handler The handle to be unregistered
* @param type (Optional) The type of a signal (if not provided, all types associated with the handle will be unregistered)
*/
unregisterObserver<T extends Signal>(
handler: SignalHandler<T>,
type?: symbol
Expand Down
14 changes: 14 additions & 0 deletions test/IndexedCollection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,18 @@ describe('mutable collection tests', () => {
);
});
});

describe('move', () => {
beforeEach(() => {
carsArrayCollection.moveBefore(usedTeslaModel3, newTeslaModelX);
});

it('The number of items in the list have not changed', () => {
expect(carsArrayCollection.count).toEqual(allCars.length);
});

it('The order of the cars has changed', () => {
expect(carsArrayCollection.items).not.toEqual(allCars);
});
});
});
21 changes: 21 additions & 0 deletions test/internals/InternalList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ describe('invalidation test', () => {
expect(list.output).not.toBe(outputBefore);
});
});

describe('moveBefore', () => {
beforeEach(() => {
list.moveBefore(7, 6);
});

test('source reorder the number according to the move', () => {
expect(list.source).toEqual([1, 7, 6]);
});
});

describe('moveAfter', () => {
beforeEach(() => {
list.moveAfter(1, 6);
});

test('source reorder the number according to the move', () => {
console.log(list.source);
expect(list.source).toEqual([6, 1, 7]);
});
});
});

describe('direct source mutation - invalidate should generate a new list after changes', () => {
Expand Down

0 comments on commit 5cff501

Please sign in to comment.