forked from mystiq/hydrogen-web
WIP - 2
This commit is contained in:
parent
66f6c4aba1
commit
04d5b9bfda
1 changed files with 44 additions and 38 deletions
|
@ -34,11 +34,11 @@ type SegmentType = {
|
||||||
"member": string;
|
"member": string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Navigation {
|
export class Navigation<T> {
|
||||||
private readonly _allowsChild: AllowsChild;
|
private readonly _allowsChild: AllowsChild;
|
||||||
private _path: Path;
|
private _path: Path<T>;
|
||||||
private readonly _observables: Map<string, SegmentObservable> = new Map();
|
private readonly _observables: Map<keyof T, SegmentObservable<T>> = new Map();
|
||||||
private readonly _pathObservable: ObservableValue<Path>;
|
private readonly _pathObservable: ObservableValue<Path<T>>;
|
||||||
|
|
||||||
constructor(allowsChild: AllowsChild) {
|
constructor(allowsChild: AllowsChild) {
|
||||||
this._allowsChild = allowsChild;
|
this._allowsChild = allowsChild;
|
||||||
|
@ -46,11 +46,11 @@ export class Navigation {
|
||||||
this._pathObservable = new ObservableValue(this._path);
|
this._pathObservable = new ObservableValue(this._path);
|
||||||
}
|
}
|
||||||
|
|
||||||
get pathObservable(): ObservableValue<Path> {
|
get pathObservable(): ObservableValue<Path<T>> {
|
||||||
return this._pathObservable;
|
return this._pathObservable;
|
||||||
}
|
}
|
||||||
|
|
||||||
get path(): Path {
|
get path(): Path<T> {
|
||||||
return this._path;
|
return this._path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ export class Navigation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPath(path: Path): void {
|
applyPath(path: Path<T>): void {
|
||||||
// Path is not exported, so you can only create a Path through Navigation,
|
// Path is not exported, so you can only create a Path through Navigation,
|
||||||
// so we assume it respects the allowsChild rules
|
// so we assume it respects the allowsChild rules
|
||||||
const oldPath = this._path;
|
const oldPath = this._path;
|
||||||
|
@ -85,7 +85,7 @@ export class Navigation {
|
||||||
this._pathObservable.set(this._path);
|
this._pathObservable.set(this._path);
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(type: keyof SegmentType): SegmentObservable {
|
observe(type: keyof T): SegmentObservable<T> {
|
||||||
let observable = this._observables.get(type);
|
let observable = this._observables.get(type);
|
||||||
if (!observable) {
|
if (!observable) {
|
||||||
observable = new SegmentObservable(this, type);
|
observable = new SegmentObservable(this, type);
|
||||||
|
@ -94,7 +94,7 @@ export class Navigation {
|
||||||
return observable;
|
return observable;
|
||||||
}
|
}
|
||||||
|
|
||||||
pathFrom(segments: Segment<any>[]): Path {
|
pathFrom(segments: Segment<any>[]): Path<T> {
|
||||||
let parent: Segment<any> | undefined;
|
let parent: Segment<any> | undefined;
|
||||||
let i: number;
|
let i: number;
|
||||||
for (i = 0; i < segments.length; i += 1) {
|
for (i = 0; i < segments.length; i += 1) {
|
||||||
|
@ -106,12 +106,12 @@ export class Navigation {
|
||||||
return new Path(segments, this._allowsChild);
|
return new Path(segments, this._allowsChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
segment<T extends keyof SegmentType>(type: T, value: SegmentType[T]): Segment<T> {
|
segment<K extends keyof T>(type: K, value: T[K]): Segment<T> {
|
||||||
return new Segment(type, value);
|
return new Segment(type, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function segmentValueEqual(a?: SegmentType[keyof SegmentType], b?: SegmentType[keyof SegmentType]): boolean {
|
function segmentValueEqual<T>(a?: T[keyof T], b?: T[keyof T]): boolean {
|
||||||
if (a === b) {
|
if (a === b) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -129,27 +129,27 @@ function segmentValueEqual(a?: SegmentType[keyof SegmentType], b?: SegmentType[k
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Segment<T extends keyof SegmentType> {
|
export class Segment<T, K extends keyof T = any> {
|
||||||
constructor(
|
constructor(
|
||||||
public type: T,
|
public type: K,
|
||||||
public value: SegmentType[T] | true = value === undefined ? true : value
|
public value: T[K] = (value === undefined ? true : value) as T[K]
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Path {
|
class Path<T> {
|
||||||
private readonly _segments: Segment<any>[];
|
private readonly _segments: Segment<T, any>[];
|
||||||
private readonly _allowsChild: AllowsChild;
|
private readonly _allowsChild: AllowsChild;
|
||||||
|
|
||||||
constructor(segments: Segment<any>[] = [], allowsChild: AllowsChild) {
|
constructor(segments: Segment<T>[] = [], allowsChild: AllowsChild) {
|
||||||
this._segments = segments;
|
this._segments = segments;
|
||||||
this._allowsChild = allowsChild;
|
this._allowsChild = allowsChild;
|
||||||
}
|
}
|
||||||
|
|
||||||
clone(): Path {
|
clone(): Path<T> {
|
||||||
return new Path(this._segments.slice(), this._allowsChild);
|
return new Path(this._segments.slice(), this._allowsChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
with(segment: Segment<any>): Path | null {
|
with(segment: Segment<T>): Path<T> | null {
|
||||||
let index = this._segments.length - 1;
|
let index = this._segments.length - 1;
|
||||||
do {
|
do {
|
||||||
if (this._allowsChild(this._segments[index], segment)) {
|
if (this._allowsChild(this._segments[index], segment)) {
|
||||||
|
@ -164,7 +164,7 @@ class Path {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
until(type: keyof SegmentType): Path {
|
until(type: keyof T): Path<T> {
|
||||||
const index = this._segments.findIndex(s => s.type === type);
|
const index = this._segments.findIndex(s => s.type === type);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
return new Path(this._segments.slice(0, index + 1), this._allowsChild)
|
return new Path(this._segments.slice(0, index + 1), this._allowsChild)
|
||||||
|
@ -172,11 +172,11 @@ class Path {
|
||||||
return new Path([], this._allowsChild);
|
return new Path([], this._allowsChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(type: keyof SegmentType): Segment<any> | undefined {
|
get(type: keyof T): Segment<T> | undefined {
|
||||||
return this._segments.find(s => s.type === type);
|
return this._segments.find(s => s.type === type);
|
||||||
}
|
}
|
||||||
|
|
||||||
replace(segment: Segment<any>): Path | null {
|
replace(segment: Segment<T>): Path<T> | null {
|
||||||
const index = this._segments.findIndex(s => s.type === segment.type);
|
const index = this._segments.findIndex(s => s.type === segment.type);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const parent = this._segments[index - 1];
|
const parent = this._segments[index - 1];
|
||||||
|
@ -192,7 +192,7 @@ class Path {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get segments(): Segment<any>[] {
|
get segments(): Segment<T>[] {
|
||||||
return this._segments;
|
return this._segments;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,19 +201,19 @@ class Path {
|
||||||
* custom observable so it always returns what is in navigation.path, even if we haven't emitted the change yet.
|
* custom observable so it always returns what is in navigation.path, even if we haven't emitted the change yet.
|
||||||
* This ensures that observers of a segment can also read the most recent value of other segments.
|
* This ensures that observers of a segment can also read the most recent value of other segments.
|
||||||
*/
|
*/
|
||||||
class SegmentObservable extends BaseObservableValue<SegmentType[keyof SegmentType] | undefined> {
|
class SegmentObservable<T> extends BaseObservableValue<T[keyof T] | undefined> {
|
||||||
private readonly _navigation: Navigation;
|
private readonly _navigation: Navigation<T>;
|
||||||
private _type: keyof SegmentType;
|
private _type: keyof T;
|
||||||
private _lastSetValue?: SegmentType[keyof SegmentType];
|
private _lastSetValue?: T[keyof T];
|
||||||
|
|
||||||
constructor(navigation: Navigation, type: keyof SegmentType) {
|
constructor(navigation: Navigation<T>, type: keyof T) {
|
||||||
super();
|
super();
|
||||||
this._navigation = navigation;
|
this._navigation = navigation;
|
||||||
this._type = type;
|
this._type = type;
|
||||||
this._lastSetValue = navigation.path.get(type)?.value;
|
this._lastSetValue = navigation.path.get(type)?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(): SegmentType[keyof SegmentType] | undefined {
|
get(): T[keyof T] | undefined {
|
||||||
const path = this._navigation.path;
|
const path = this._navigation.path;
|
||||||
const segment = path.get(this._type);
|
const segment = path.get(this._type);
|
||||||
const value = segment?.value;
|
const value = segment?.value;
|
||||||
|
@ -222,7 +222,7 @@ class SegmentObservable extends BaseObservableValue<SegmentType[keyof SegmentTyp
|
||||||
|
|
||||||
emitIfChanged(): void {
|
emitIfChanged(): void {
|
||||||
const newValue = this.get();
|
const newValue = this.get();
|
||||||
if (!segmentValueEqual(newValue, this._lastSetValue)) {
|
if (!segmentValueEqual<T>(newValue, this._lastSetValue)) {
|
||||||
this._lastSetValue = newValue;
|
this._lastSetValue = newValue;
|
||||||
this.emit(newValue);
|
this.emit(newValue);
|
||||||
}
|
}
|
||||||
|
@ -249,7 +249,7 @@ export function tests() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function observeTypes(nav, types) {
|
function observeTypes(nav, types) {
|
||||||
const changes = [];
|
const changes: {type:string, value:any}[] = [];
|
||||||
for (const type of types) {
|
for (const type of types) {
|
||||||
nav.observe(type).subscribe(value => {
|
nav.observe(type).subscribe(value => {
|
||||||
changes.push({type, value});
|
changes.push({type, value});
|
||||||
|
@ -258,6 +258,12 @@ export function tests() {
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SegmentType = {
|
||||||
|
"foo": number;
|
||||||
|
"bar": number;
|
||||||
|
"baz": number;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"applying a path emits an event on the observable": assert => {
|
"applying a path emits an event on the observable": assert => {
|
||||||
const nav = createMockNavigation();
|
const nav = createMockNavigation();
|
||||||
|
@ -275,18 +281,18 @@ export function tests() {
|
||||||
assert.equal(changes[1].value, 8);
|
assert.equal(changes[1].value, 8);
|
||||||
},
|
},
|
||||||
"path.get": assert => {
|
"path.get": assert => {
|
||||||
const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
const path = new Path<SegmentType>([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
||||||
assert.equal(path.get("foo").value, 5);
|
assert.equal(path.get("foo")!.value, 5);
|
||||||
assert.equal(path.get("bar").value, 6);
|
assert.equal(path.get("bar")!.value, 6);
|
||||||
},
|
},
|
||||||
"path.replace success": assert => {
|
"path.replace success": assert => {
|
||||||
const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
const path = new Path<SegmentType>([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
||||||
const newPath = path.replace(new Segment("foo", 1));
|
const newPath = path.replace(new Segment("foo", 1));
|
||||||
assert.equal(newPath.get("foo").value, 1);
|
assert.equal(newPath!.get("foo")!.value, 1);
|
||||||
assert.equal(newPath.get("bar").value, 6);
|
assert.equal(newPath!.get("bar")!.value, 6);
|
||||||
},
|
},
|
||||||
"path.replace not found": assert => {
|
"path.replace not found": assert => {
|
||||||
const path = new Path([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
const path = new Path<SegmentType>([new Segment("foo", 5), new Segment("bar", 6)], () => true);
|
||||||
const newPath = path.replace(new Segment("baz", 1));
|
const newPath = path.replace(new Segment("baz", 1));
|
||||||
assert.equal(newPath, null);
|
assert.equal(newPath, null);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue