0

I have problem with understanding rxjs Observable cooperation with Angular components and their lifecycles (?). I will now describe code pasted below.

I got CartComponent, which holds array of files that have been added to cart. There is service that provides observables that add or remove files from filesArray (i use checkbox to add/remove). After adding a couple of files to cart, I expected that I would be able to use filesArray in SearchComponent (I used DI). Unfortunately, filesArray is empty in that scope, despite it's content is displayed in cart view properly. I don't understand such behavoir.

Why array is empty and how can I fix that problem? Please help.

CartComponent:

@Component({
  selector: 'app-cart',
  templateUrl: './cart.component.html',
  styleUrls: ['./cart.component.css']
})
export class CartComponent implements OnInit, OnDestroy {
  filesArray: file[] = [];

  constructor(private modalService: NgbModal, private cartService: CartService) {
  }

  open(content) {
    this.modalService.open(content);
  }

  ngOnInit(): void {
    this.cartService.addFileToCart$.pipe(
      takeUntil(this.componentDestroyed)
    ).subscribe(file => {
      this.filesArray = [...this.filesArray, file];
    });

    this.cartService.removeFileFromCart$.pipe(
      takeUntil(this.componentDestroyed)
    ).subscribe(file => {
      const fileIndex = this.filesArray.indexOf(file);
      this.filesArray.splice(fileIndex,1);
    });
  }

  private componentDestroyed: Subject<void> = new Subject();

  ngOnDestroy(): void {
    this.componentDestroyed.next();
    this.componentDestroyed.unsubscribe();
  }

}

SearchComponent:

@Component({
  selector: 'app-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.css']
})
export class SearchComponent implements OnInit {

  constructor(private cartComponent: CartComponent) {
  }

  private checkOrderedFiles() {
    console.log(this.cartComponent.pcFilesArray); //empty
  }

CartService:

@Injectable({
  providedIn: 'root'
})
export class CartService {

  addFileToCart$: Subject<PCFile> = new Subject();
  removeFileFromCart$: Subject<PCFile> = new Subject();

  constructor() { }
}

Checkbox change event handler - I emit values to subjects here:

addOrRemoveFileFromCart(checkbox, item) {
  if(checkbox.checked) {
    this.cartService.addFileToCart$.next(item);
  } else {
    this.cartService.removeFileFromCart$.next(item);
  }
}

EDIT:

public readonly files = this.cartService.addFileToCart$.pipe(
    scan((filesArray: any, file) => [...filesArray, file], []),
    share()
);

template

<div *ngFor="let file of files | async">
  {{file.id}}
</div>
2
  • There's no values being emitted on the Subject, did you forget to add a snippet of a file? Also, be aware of using Subjects without a good reason or without knowing what it exactly does. They can be abused easily and there's plenty of creation operators able to do the exact same thing. Just be mindful when using them and try not to expose too much of their API to your components and services. Commented May 7, 2019 at 13:00
  • @Bjorn'Bjeaurn'S I added that snippet. As i mentioned in question it's working in the matter of presentation in view - the array is empty only when i try to access it from another component. Commented May 7, 2019 at 13:24

2 Answers 2

1

Do not use Observables that way in CartComponent. Instead write:

@Component({
  selector: 'app-cart',
  templateUrl: './cart.component.html',
  styleUrls: ['./cart.component.css']
})
export class CartComponent {
  public readonly files = biscan(
      this.cartService.addFileToCart$,
      this.cartService.removeFileFromCart$,
      (filesArray, file) => [filesArray, file],
      (filesArray, file) => filesArray.filter(f => f !== file),
      []
    ),
    startWith([]),
  );

  constructor(private modalService: NgbModal, private cartService: CartService) {
  }

  open(content) {
    this.modalService.open(content);
  }

}

In the template use files | async wherever you are currently using fileArray.

In other components, use files as an Observable wherever you are currently using fileArray or pcFileArray.

The underlying issue is that checkOrderedFiles is never invoked — if you write code invoking it, that code never knows when fileArray has changed.

Intuitively, you are trying to "escape" from the Observable, to get your changes back into the static world of the component. That just isn't possible. Once a calculation is in the asynchronous land of Observables, it stays there.


Unfortunately biscan() (a two-Observable variation of scan) is not currently in the library, but you can write it as

const biscan = (leftObs, rightObs, leftFcn, rightFcn, initialValue) => 
   new Observable(observer => {
     let acc = initialValue;
     // this function must be called *twice* (once for left,
     // once for right) before the observer is completed.
     let complete = () => {
       complete = () => observer.complete();
     };
     const makeSub = (obs, f) => obs.subscribe(v => {
         acc = f(acc, v);
         observer.next(acc);
       },
       e => observer.error(e),
       () => complete()
     );
     const leftSub  = makeSub(leftObs, leftFcn);
     const rightSub = makeSub(rightObs, rightFcn);
     return () => { 
       leftSub.unsubscribe();
       rightSub.unsubscribe();
     };
   });

Edit: fixed typos in biscan()

Sign up to request clarification or add additional context in comments.

Comments

0

I resolved that issue with help of Malvolio (wouldn't figure it out by myself for sure). I post working code below, because i suppose that piece posted had some syntax errors. I was having a problem with async pipe, i believe it's because content of CartComponent template is inside modal. I resolved it by manually subscribing.

export class CartComponent implements OnInit, OnDestroy {
    filesArray;
    private componentDestroyed: Subject<void> = new Subject();

    biscan = (leftObs, rightObs, leftFcn, rightFcn, initialValue) =>
      new Observable(observer => {
        let acc = initialValue;
        let complete = () => observer.complete();
        const makeSub = (obs, f) => obs.subscribe(v => {
            acc = f(acc, v);
            observer.next(acc);
          },
          e => observer.error(e),
          () => complete()
        );
        const leftSub = makeSub(leftObs, leftFcn);
        const rightSub = makeSub(rightObs, rightFcn);
        return () => {
          leftSub.unsubscribe();
          rightSub.unsubscribe();
        };
      });

    public readonly files = this.biscan(
      this.cartService.addFileToCart$,
      this.cartService.removeFileFromCart$,
      (filesArray, file) => {
        filesArray.push(file);
        return filesArray;
      },
      (filesArray, file) => {
        filesArray.splice(filesArray.indexOf(file), 1);
        return filesArray;
      },
      []
    ).pipe(takeUntil(this.componentDestroyed));

    constructor(private cartService: CartService) {}

    ngOnInit(): void {
        this.files.subscribe(files => this.filesArray = files);  
    }

    ngOnDestroy(): void {
        this.componentDestroyed.next();
        this.componentDestroyed.unsubscribe();
    }
}

6 Comments

Two issues, one minor, one major. The minor one, you re-wrote the complete function so it completes the observer the first time it's called. Originally it worked only the second time. Your way, if either of the source observables ends, the scan ends. That probably doesn't matter in the current implementation, but in general, it isn't what you want. I put a Typescript version here so you can experiment with it.
The major issue is that you keep trying to update a field filesArray in the Component. This will end in tears. Read this article by the author of RxJS and this one by yours truly to see why you shouldn't be doing this and use the async pipe instead.
Async pipe doesn't work inside modal. Unfortunately, it's requirement to have it in modal. When i put it outside everything goes smoothly. I think the problem is that element isn't in DOM before opening modal and therefore async pipe has no chance to work. Do you have idea how to make it without having to subscribe to field?
Huh? Why wouldn't async pipe doesn't work inside modal? It works fine.
Yes it does, but my case is when i emit value to files (stream made from this.biscan), async pipe inside modal doesn't exist in DOM, therefore it's not subscribing to values at the moment of emission, and that results in modal being empty. At first i approached this issue with field in CartComponent and subscribing to emitted values to it. Today i came up with another idea - i used ReplaySubject inside modal, so when modal is opening, current array is emitted (ReplaySubject with buffer size 1 feature), which is correct for that business case. Is that technically correct solution?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.