3
\$\begingroup\$

I want to implement my own custom observable class so I can have a hierarchy of components and the closest defined configuration item bubbles to the front.

<form controlClasses="formDefinedClass">
  <group controlClasses="groupDefinedClass">
    <control controlClasses="controlDefinedClass"></control>
  </group>
</form>

The setter property of these inputs then passes the value to the next method of an observable.

@Input()
set controlClasses(value: string) {
  this.controlClasses$.next(value);
}

My goal is to have the closest config push to the front. In the above example on the control the controlClasses observable will emit 'controlDefinedClass' but if I was to remove the property it would emit 'groupDefinedClass' as it is the closest config in the control hierarchy.

I have spent a bit of time looking through the source code of the rxjs repository and was looking for some advise on implementing my own observable classes. To solve the above requirement I have come up with two data structures.

One is the StackSubject.

import { Observable, Subject, Subscriber, Subscription, SubscriptionLike, ObjectUnsubscribedError } from 'rxjs';

import { StackSubjectSubscription } from './stack-subject-subscription';

export class StackSubject<T> extends Subject<T> {

  constructor(private _value: T, private defaultValue: T, private parent$?: Observable<T>) {
    super();
  }

  private parentSubscription: Subscription;

  private parentValue: T;

  get value(): T {
    return this.getValue();
  }

  complete() {
    super.complete();
    this.parentUnsubscribe();
  }

  unsubscribe() {
    super.unsubscribe();
    this.parentUnsubscribe();
  }

  parentUnsubscribe() {
    if (this.parentSubscription) {
      this.parentSubscription.unsubscribe();
    }
    this.parentSubscription = undefined;
  }

  /** @deprecated This is an internal implementation detail, do not use. */
  _subscribe(subscriber: Subscriber<T>): Subscription {
    if (!this.parentSubscription && this.parent$ && !this.closed && !this.hasError && !this.isStopped) {
      this.parentSubscription = this.parent$.subscribe(value => {
        this.parentValue = value;
        super.next(this.calcValue());
      });
    }
    const subscription = super._subscribe(subscriber);
    if (subscription && !(<SubscriptionLike>subscription).closed) {
      subscriber.next(this.calcValue());
    }
    return new StackSubjectSubscription(this, subscription);
  }

  getValue(): T {
    if (this.hasError) {
      throw this.thrownError;
    } else if (this.closed) {
      throw new ObjectUnsubscribedError();
    } else {
      return this.calcValue();
    }
  }

  private calcValue() {
    return this._value || this.parentValue || this.defaultValue;
  }

  next(value: T): void {
    this._value = value;
    super.next(this.calcValue());
  }
}

Demo at https://stackblitz.com/edit/angular-tdajza?file=src%2Fapp%2Fstack-subject.ts

The other is the PushStack.

import { Observable, BehaviorSubject, Subscription, SubscriptionLike, combineLatest, Subscriber } from 'rxjs';
import { map } from 'rxjs/operators';

export class PushStack<T> extends Observable<T> implements SubscriptionLike {
  private behaviorSubject$ = new BehaviorSubject(this._value);

  private observable$ = this.parent$ ?
    combineLatest(this.behaviorSubject$, this.parent$).pipe(
      map(([behaviorSubject, parent]) => behaviorSubject || parent || this.defaultValue)
    ) :
      this.behaviorSubject$.pipe(map(behaviorSubject => behaviorSubject || this.defaultValue));

  get closed(): boolean {
    return this.behaviorSubject$.closed;
  }

  constructor(private _value: T, private defaultValue: T, private parent$?: Observable<T>) {
    super();
  }

  complete() {
    this.behaviorSubject$.complete();
  }

  unsubscribe() {
    this.behaviorSubject$.unsubscribe();
  }

  _subscribe(subscriber: Subscriber<T>): Subscription {
    return this.observable$.subscribe(subscriber);
  }

  next(value: T): void {
    this.behaviorSubject$.next(value);
  }
}

Demo at https://stackblitz.com/edit/angular-cnrttk?file=src%2Fapp%2Fpush-stack.ts

Should I be implementing the _subscribe method? The rxjs classes for subjects and behavour subjects do so but also have the comment /** @deprecated This is an internal implementation detail, do not use. */ Is this implementation detail likely to change in a future version of rxjs?

Is having a subject that subscribes to another observable as in my StackSubject good practice?

Is creating a private observable and exposing the subscribe of the private observable to implement my observable as in my PushStack good practice?

The two methods both fulfil my requirements but I personally prefer the push stack method but the stack subject offers the additional benefit of having the getValue() method and value property so you can get the closest push value without needing to subscribe.

Are there any devs who have implemented custom observable classes or have worked on the rxjs repository who can provide an opinion on my implementation of these observables?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.