move observable-api to libs
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { distinctUntilChanged, map, Observable, shareReplay } from "rxjs";
|
||||
import { MappingFunction, MemoizationFunction, defaultMemoization } from "./unique-behavior-subject";
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @method createObservablePart
|
||||
* @param {Observable<T>} source - RxJS Subject to use for this Observable.
|
||||
* @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return.
|
||||
* @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different.
|
||||
* @description - Creates a RxJS Observable from RxJS Subject.
|
||||
* @example <caption>Example create a Observable for part of the data Subject.</caption>
|
||||
* public readonly myPart = CreateObservablePart(this._data, (data) => data.myPart);
|
||||
*/
|
||||
|
||||
export function createObservablePart<T, R>(
|
||||
source$: Observable<T>,
|
||||
mappingFunction: MappingFunction<T, R>,
|
||||
memoizationFunction?: MemoizationFunction<R>
|
||||
): Observable<R> {
|
||||
return source$.pipe(
|
||||
map(mappingFunction),
|
||||
distinctUntilChanged(memoizationFunction || defaultMemoization),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
6
src/Umbraco.Web.UI.Client/libs/observable-api/index.ts
Normal file
6
src/Umbraco.Web.UI.Client/libs/observable-api/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './observer.controller';
|
||||
export * from './observer';
|
||||
export * from './unique-behavior-subject';
|
||||
export * from './unique-array-behavior-subject';
|
||||
export * from './unique-object-behavior-subject';
|
||||
export * from './create-observable-part.method'
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { UmbObserver } from './observer';
|
||||
import { UmbControllerInterface, UmbControllerHostInterface } from '@umbraco-cms/controller';
|
||||
|
||||
export class UmbObserverController<T> extends UmbObserver<T> implements UmbControllerInterface {
|
||||
_alias?: string;
|
||||
public get unique() {
|
||||
return this._alias;
|
||||
}
|
||||
|
||||
constructor(host: UmbControllerHostInterface, source: Observable<T>, callback: (_value: T) => void, alias?: string) {
|
||||
super(source, callback);
|
||||
this._alias = alias;
|
||||
|
||||
// Lets check if controller is already here:
|
||||
/*
|
||||
if (this._subscriptions.has(source)) {
|
||||
const subscription = this._subscriptions.get(source);
|
||||
subscription?.unsubscribe();
|
||||
}
|
||||
*/
|
||||
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
22
src/Umbraco.Web.UI.Client/libs/observable-api/observer.ts
Normal file
22
src/Umbraco.Web.UI.Client/libs/observable-api/observer.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
export class UmbObserver<T> {
|
||||
|
||||
#subscription!: Subscription;
|
||||
|
||||
constructor(source: Observable<T>, callback: (_value: T) => void) {
|
||||
|
||||
this.#subscription = source.subscribe((value) => callback(value));
|
||||
}
|
||||
|
||||
// Notice controller class implements empty hostConnected().
|
||||
|
||||
hostDisconnected() {
|
||||
this.#subscription.unsubscribe();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.#subscription.unsubscribe();
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { createObservablePart } from '@umbraco-cms/observable-api';
|
||||
import { UniqueArrayBehaviorSubject } from './unique-array-behavior-subject';
|
||||
|
||||
describe('UniqueArrayBehaviorSubject', () => {
|
||||
|
||||
type ObjectType = {key: string, another: string};
|
||||
type ArrayType = ObjectType[];
|
||||
|
||||
let subject: UniqueArrayBehaviorSubject<ObjectType>;
|
||||
let initialData: ArrayType;
|
||||
|
||||
beforeEach(() => {
|
||||
initialData = [
|
||||
{key: '1', another: 'myValue1'},
|
||||
{key: '2', another: 'myValue2'},
|
||||
{key: '3', another: 'myValue3'}
|
||||
];
|
||||
subject = new UniqueArrayBehaviorSubject(initialData, (a, b) => a.key === b.key);
|
||||
});
|
||||
|
||||
|
||||
it('replays latests, no matter the amount of subscriptions.', (done) => {
|
||||
|
||||
const observer = subject.asObservable();
|
||||
observer.subscribe((value) => {
|
||||
expect(value).to.be.equal(initialData);
|
||||
});
|
||||
observer.subscribe((value) => {
|
||||
expect(value).to.be.equal(initialData);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('add new item via appendOne method.', (done) => {
|
||||
|
||||
const newItem = {key: '4', another: 'myValue4'};
|
||||
subject.appendOne(newItem);
|
||||
|
||||
const expectedData = [...initialData, newItem]
|
||||
|
||||
const observer = subject.asObservable();
|
||||
observer.subscribe((value) => {
|
||||
expect(value.length).to.be.equal(expectedData.length);
|
||||
expect(value[3].another).to.be.equal(expectedData[3].another);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('createObservablePart for a specific entry of array', (done) => {
|
||||
|
||||
const subObserver = createObservablePart(subject, data => data.find(x => x.key === '2'));
|
||||
subObserver.subscribe((entry) => {
|
||||
if(entry) {
|
||||
expect(entry.another).to.be.equal(initialData[1].another);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('createObservablePart returns undefined if item does not exist', (done) => {
|
||||
|
||||
let amountOfCallbacks = 0;
|
||||
const newItem = {key: '4', another: 'myValue4'};
|
||||
|
||||
const subObserver = createObservablePart(subject, data => data.find(x => x.key === newItem.key));
|
||||
subObserver.subscribe((entry) => {
|
||||
amountOfCallbacks++;
|
||||
if(amountOfCallbacks === 1) {
|
||||
expect(entry).to.be.equal(undefined);// First callback should give null, cause we didn't have this entry when the subscription was made.
|
||||
}
|
||||
if(amountOfCallbacks === 2) {
|
||||
expect(entry).to.be.equal(newItem);// Second callback should give us the right data:
|
||||
if(entry) {
|
||||
expect(entry.another).to.be.equal(newItem.another);
|
||||
done();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
subject.appendOne(newItem);
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('asObservable returns the replaced item', (done) => {
|
||||
|
||||
const newItem = {key: '2', another: 'myValue4'};
|
||||
subject.appendOne(newItem);
|
||||
|
||||
const expectedData = [initialData[0], newItem, initialData[2]];
|
||||
|
||||
const observer = subject.asObservable();
|
||||
observer.subscribe((value) => {
|
||||
expect(value.length).to.be.equal(expectedData.length);
|
||||
expect(value[1].another).to.be.equal(newItem.another);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('createObservablePart returns the replaced item', (done) => {
|
||||
|
||||
const newItem = {key: '2', another: 'myValue4'};
|
||||
subject.appendOne(newItem);
|
||||
|
||||
const subObserver = createObservablePart(subject, data => data.find(x => x.key === newItem.key));
|
||||
subObserver.subscribe((entry) => {
|
||||
expect(entry).to.be.equal(newItem);// Second callback should give us the right data:
|
||||
if(entry) {
|
||||
expect(entry.another).to.be.equal(newItem.another);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { appendToFrozenArray, UniqueBehaviorSubject } from "./unique-behavior-subject";
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @class UniqueObjectBehaviorSubject
|
||||
* @extends {UniqueBehaviorSubject<T>}
|
||||
* @description - A RxJS UniqueObjectBehaviorSubject which deepFreezes the object-data to ensure its not manipulated from any implementations.
|
||||
* Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content.
|
||||
*
|
||||
* The UniqueObjectBehaviorSubject provides methods to append data when the data is an Object.
|
||||
*/
|
||||
|
||||
export class UniqueArrayBehaviorSubject<T> extends UniqueBehaviorSubject<T[]> {
|
||||
|
||||
|
||||
constructor(initialData: T[], private _uniqueCompare?: (existingEntry: T, newEntry: T) => boolean) {
|
||||
super(initialData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @method append
|
||||
* @param {Partial<T>} partialData - A object containing some of the data for this Subject.
|
||||
* @description - Append some new data to this Object.
|
||||
* @example <caption>Example append some data.</caption>
|
||||
* const data = [
|
||||
* { key: 1, value: 'foo'},
|
||||
* { key: 2, value: 'bar'}
|
||||
* ];
|
||||
* const mySubject = new UniqueArrayBehaviorSubject(data);
|
||||
* mySubject.append({ key: 1, value: 'replaced-foo'});
|
||||
*/
|
||||
appendOne(entry: T) {
|
||||
this.next(appendToFrozenArray(this.getValue(), entry, this._uniqueCompare))
|
||||
}
|
||||
|
||||
/**
|
||||
* @method append
|
||||
* @param {T[]} entries - A array of new data to be added in this Subject.
|
||||
* @description - Append some new data to this Object, if it compares to existing data it will replace it.
|
||||
* @example <caption>Example append some data.</caption>
|
||||
* const data = [
|
||||
* { key: 1, value: 'foo'},
|
||||
* { key: 2, value: 'bar'}
|
||||
* ];
|
||||
* const mySubject = new UniqueArrayBehaviorSubject(data);
|
||||
* mySubject.append([
|
||||
* { key: 1, value: 'replaced-foo'},
|
||||
* { key: 3, value: 'another-bla'}
|
||||
* ]);
|
||||
*/
|
||||
append(entries: T[]) {
|
||||
entries.forEach(x => this.appendOne(x))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { UniqueBehaviorSubject } from './unique-behavior-subject';
|
||||
import { createObservablePart } from '@umbraco-cms/observable-api';
|
||||
|
||||
describe('UniqueBehaviorSubject', () => {
|
||||
|
||||
type ObjectType = {key: string, another: string};
|
||||
|
||||
let subject: UniqueBehaviorSubject<ObjectType>;
|
||||
let initialData: ObjectType;
|
||||
|
||||
beforeEach(() => {
|
||||
initialData = {key: 'some', another: 'myValue'};
|
||||
subject = new UniqueBehaviorSubject(initialData);
|
||||
});
|
||||
|
||||
|
||||
it('getValue gives the initial data', () => {
|
||||
expect(subject.value.another).to.be.equal(initialData.another);
|
||||
});
|
||||
|
||||
it('update via next', () => {
|
||||
subject.next({key: 'some', another: 'myNewValue'});
|
||||
expect(subject.value.another).to.be.equal('myNewValue');
|
||||
});
|
||||
|
||||
it('replays latests, no matter the amount of subscriptions.', (done) => {
|
||||
|
||||
const observer = subject.asObservable();
|
||||
observer.subscribe((value) => {
|
||||
expect(value).to.be.equal(initialData);
|
||||
});
|
||||
observer.subscribe((value) => {
|
||||
expect(value).to.be.equal(initialData);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('use createObservablePart, updates on its specific change.', (done) => {
|
||||
|
||||
let amountOfCallbacks = 0;
|
||||
|
||||
const subObserver = createObservablePart(subject, data => data.another);
|
||||
subObserver.subscribe((value) => {
|
||||
amountOfCallbacks++;
|
||||
if(amountOfCallbacks === 1) {
|
||||
expect(value).to.be.equal('myValue');
|
||||
}
|
||||
if(amountOfCallbacks === 2) {
|
||||
expect(value).to.be.equal('myNewValue');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
subject.next({key: 'change_this_first_should_not_trigger_update', another: 'myValue'});
|
||||
subject.next({key: 'some', another: 'myNewValue'});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
|
||||
// TODO: Should this handle array as well?
|
||||
function deepFreeze<T>(inObj: T): T {
|
||||
if(typeof inObj === 'object') {
|
||||
Object.freeze(inObj);
|
||||
|
||||
Object.getOwnPropertyNames(inObj)?.forEach(function (prop) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if ((inObj as any).hasOwnProperty(prop)
|
||||
&& (inObj as any)[prop] != null
|
||||
&& typeof (inObj as any)[prop] === 'object'
|
||||
&& !Object.isFrozen((inObj as any)[prop])) {
|
||||
deepFreeze((inObj as any)[prop]);
|
||||
}
|
||||
});
|
||||
}
|
||||
return inObj;
|
||||
}
|
||||
|
||||
|
||||
export function naiveObjectComparison(objOne: any, objTwo: any): boolean {
|
||||
return JSON.stringify(objOne) === JSON.stringify(objTwo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @method appendToFrozenArray
|
||||
* @param {Observable<T>} source - RxJS Subject to use for this Observable.
|
||||
* @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return.
|
||||
* @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different.
|
||||
* @description - Creates a RxJS Observable from RxJS Subject.
|
||||
* @example <caption>Example append new entry for a UniqueBehaviorSubject which is an array. Where the key is unique and the item will be updated if matched with existing.</caption>
|
||||
* const entry = {key: 'myKey', value: 'myValue'};
|
||||
* const newDataSet = appendToFrozenArray(mySubject.getValue(), entry, x => x.key === key);
|
||||
* mySubject.next(newDataSet);
|
||||
*/
|
||||
export function appendToFrozenArray<T>(data: T[], entry: T, uniqueMethod?: (existingEntry: T, newEntry: T) => boolean): T[] {
|
||||
const unFrozenDataSet = [...data];
|
||||
if(uniqueMethod) {
|
||||
const indexToReplace = unFrozenDataSet.findIndex((x) => uniqueMethod(x, entry));
|
||||
if(indexToReplace !== -1) {
|
||||
unFrozenDataSet[indexToReplace] = entry;
|
||||
} else {
|
||||
unFrozenDataSet.push(entry);
|
||||
}
|
||||
} else {
|
||||
unFrozenDataSet.push(entry);
|
||||
}
|
||||
return unFrozenDataSet;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export type MappingFunction<T, R> = (mappable: T) => R;
|
||||
export type MemoizationFunction<R> = (previousResult: R, currentResult: R) => boolean;
|
||||
|
||||
export function defaultMemoization(previousValue: any, currentValue: any): boolean {
|
||||
if (typeof previousValue === 'object' && typeof currentValue === 'object') {
|
||||
return naiveObjectComparison(previousValue, currentValue);
|
||||
}
|
||||
return previousValue === currentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @class UniqueBehaviorSubject
|
||||
* @extends {BehaviorSubject<T>}
|
||||
* @description - A RxJS BehaviorSubject which deepFreezes the data to ensure its not manipulated from any implementations.
|
||||
* Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content.
|
||||
*/
|
||||
export class UniqueBehaviorSubject<T> extends BehaviorSubject<T> {
|
||||
constructor(initialData: T) {
|
||||
super(deepFreeze(initialData));
|
||||
}
|
||||
|
||||
next(newData: T): void {
|
||||
const frozenData = deepFreeze(newData);
|
||||
// Only update data if its different than current data.
|
||||
if (!naiveObjectComparison(frozenData, this.getValue())) {
|
||||
super.next(frozenData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { UniqueObjectBehaviorSubject } from './unique-object-behavior-subject';
|
||||
import { createObservablePart } from '@umbraco-cms/observable-api';
|
||||
|
||||
describe('UniqueObjectBehaviorSubject', () => {
|
||||
|
||||
type ObjectType = {key: string, another: string};
|
||||
|
||||
let subject: UniqueObjectBehaviorSubject<ObjectType>;
|
||||
let initialData: ObjectType;
|
||||
|
||||
beforeEach(() => {
|
||||
initialData = {key: 'some', another: 'myValue'};
|
||||
subject = new UniqueObjectBehaviorSubject(initialData);
|
||||
});
|
||||
|
||||
|
||||
it('replays latests, no matter the amount of subscriptions.', (done) => {
|
||||
|
||||
const observer = subject.asObservable();
|
||||
observer.subscribe((value) => {
|
||||
expect(value).to.be.equal(initialData);
|
||||
});
|
||||
observer.subscribe((value) => {
|
||||
expect(value).to.be.equal(initialData);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('use createObservablePart, updates on its specific change.', (done) => {
|
||||
|
||||
let amountOfCallbacks = 0;
|
||||
|
||||
const subObserver = createObservablePart(subject, data => data.another);
|
||||
subObserver.subscribe((value) => {
|
||||
amountOfCallbacks++;
|
||||
if(amountOfCallbacks === 1) {
|
||||
expect(value).to.be.equal('myValue');
|
||||
}
|
||||
if(amountOfCallbacks === 2) {
|
||||
expect(value).to.be.equal('myNewValue');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
subject.update({key: 'change_this_first_should_not_trigger_update'});
|
||||
subject.update({another: 'myNewValue'});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { UniqueBehaviorSubject } from "./unique-behavior-subject";
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @class UniqueObjectBehaviorSubject
|
||||
* @extends {UniqueBehaviorSubject<T>}
|
||||
* @description - A RxJS UniqueObjectBehaviorSubject which deepFreezes the object-data to ensure its not manipulated from any implementations.
|
||||
* Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content.
|
||||
*
|
||||
* The UniqueObjectBehaviorSubject provides methods to append data when the data is an Object.
|
||||
*/
|
||||
export class UniqueObjectBehaviorSubject<T> extends UniqueBehaviorSubject<T> {
|
||||
|
||||
/**
|
||||
* @method append
|
||||
* @param {Partial<T>} partialData - A object containing some of the data for this Subject.
|
||||
* @description - Append some new data to this Object.
|
||||
* @example <caption>Example append some data.</caption>
|
||||
* const data = {key: 'myKey', value: 'myInitialValue'};
|
||||
* const mySubject = new UniqueObjectBehaviorSubject(data)
|
||||
* mySubject.append({value: 'myNewValue'})
|
||||
*/
|
||||
update(partialData: Partial<T>) {
|
||||
this.next({ ...this.getValue(), ...partialData });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user