5

I want to use an Observable<string[]> for gradually displaying content on component of my Angular apps.

  1. On TypeScript side I declared this:
export class ResultComponent implements OnInit{
 
 message: string = 'my message for user';
 spreadedMessage$: Observable<string> = from(this.message);
 progressiveMessage:string = "";

 ngOnInit() {
    let interval = 1
    this.obsMessage$
      .pipe(
        tap((letter) => {
           delay(35*interval),
           this.progressiveMessage += letter;
          interval++
        })
      )
      .subscribe();
    }
}
  1. Binding progressiveMessage to the template

<div> {{ progressiveMessage }} </div>

My full code listing can be found here


I tried a second approach that works well as below but I would like to understand what is wrong with my use of Observable to progress.

My alternative solution:

  1. Changed spreadedMessage$: Observable<string> = from(this.message); to spreadedMessage: string[] = [...this.message];

  2. Declared this on OnInit() method:

for (let i: number = 0; i < this.spreadedMessage.length; i++) {
        setTimeout(() => (this.spaceMessager += this.spreadedMessage[i]), 35 * i);    
}

Any ideas?

Julius A
  • 38,062
  • 26
  • 74
  • 96
  • 2
    It would be helpful if you could provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) showing your problem. Ideally in an online code editor such as [Stackblitz](https://www.stackblitz.com). – Cuzy Apr 11 '23 at 13:09

2 Answers2

4

You are making a very common mistake that many people that are new to RxJS and Observables does; mixing imperative and reactive programming. The idea of the reactive programming is that your observable is the source of truth that the data is displayed from.

Code like

this.progressiveMessage += letter;

inside an operator where you assign the new value to another variable is an anti pattern of observables. Instead, use operators to modify and return the new value.

Since you are using Angular, the idea is that you never have to subscribe manually in your code, but instead use the async pipe in the template.

What you want to do is to create an observable that returns a new value every time the template should be updated.

The progressiveMessage$ in my code below is an Observable<string>, and we assign it a value in the ngOnInit function.

sentence: string = 'je suis un messsage qui apparait progressivement';
spreadedMessage$: Observable<string> = from(this.sentence);
progressiveMessage$: Observable<string>;

ngOnInit() {
this.progressiveMessage$ = from( // We use the from operator to get a stream from an array
  this.sentence.split('') // Lets split the message up into each character for display purposes
).pipe(
  // here, we first use the concatMap operator to project each source value to a new observable with the of operator
  // we then delay that observable with 250ms, meaning that each observable from the letter array is delayed with 250ms
  concatMap((letter) => of(letter).pipe(delay(250))), 
  // we use the scan operator (much like a regular Array.reduce) to iterate over the values and append the last one to the previous one
  scan((acc, val) => {
    // we return the value here every time a new letter arrives
    return acc + val;
  })
);
}

Here is a working Stackblitz example for you!

Daniel B
  • 8,770
  • 5
  • 43
  • 76
2

In your first example code, delay() is not doing anything! You have included it inside of the tap operator, which is not correct. It needs to be directly within the .pipe().

However, even if you placed it correctly, I think will be tricky to get it to work as you desire because the delay duration is not evaluated for every emission, it's only evaluated once with the initial value of interval,

I think something like this would work:

  ngOnInit() {
    this.spreadedMessage$.pipe(
      concatMap(letter => of(letter).pipe(delay(35))),
      tap(letter => this.progressiveMessage += letter),
    ).subscribe();
  }

Here, we return a new observable inside concatMap for each emission, this allows the delay interval to be evaluated for every emitted letter.


However, if you are simply trying to emit the characters one at a time at a constant interval, it's probably easier to use the interval creator function like this:

    interval(35).pipe(
      map(i => this.message[i]),
      take(this.message.length),
      tap(letter => this.progressiveMessage += letter),
    ).subscribe();

Since interval emits increasing integers starting at 0 we can use that as the index of the character we want to emit.

We use the take operator end the stream once the number of emissions has reached the length of the message.


To simplify further, we could alleviate the outside variable progressiveMessage variable and just emit the portion of the message string we are interested in.

  interval(35).pipe(
    map(i => this.message.slice(0, i+1)),
    take(this.message.length),
  ).subscribe();

And, if you want to simplify even further, you can define your value as an observable and leverage the async pipe in the template. This eliminates subscribing and the use of ngOnInit:

  progressiveMessage$ = interval(35).pipe(
    map(i => this.message.slice(0, i+1)),
    take(this.message.length),
  );
<p>
  {{ progressiveMessage$ | async }}
</p>

Here's a little StackBlitz example.

BizzyBob
  • 12,309
  • 4
  • 27
  • 51