【问题标题】:ngrx, rxjs, and angular 5ngrx、rxjs 和 Angular 5
【发布时间】:2018-04-20 16:09:43
【问题描述】:

我试图找出一个简单的计时器可以观察几个星期,但没有运气。我最初是在上周发布的:ngrx and angular 5 on stackoverflow 并没有得到任何地方。我尝试实施建议并使用我的原始解决方案更进一步。此时我有一个计时器,它发出并输出倒计时,但仅在单击播放或暂停按钮时。我试图让倒计时继续在按下播放按钮时向显示组件发出值。我已经控制台记录了计时器,它在播放被推送时发出值,但显示组件没有。我想不通。我是 Angular 5 和 ngrx/rxjs 的新手。

我在 Stackblitz 上以工作形式提供了项目代码。 I have the project code available in a working form on Stackblitz here.

您可以使用用户登录:test 密码:test

定时器代码在 core/services/pomo-timer.ts

容器组件是books/containers/selected-book-page.ts

展示组件为books/components/book-detail.ts

此时它应该显示 6 秒,一旦按下播放按钮,它应该发出并显示每秒倒计时,直到按下 暂停 按钮,此时它应该暂停直到 播放 再次被点击。正如我所提到的,当我 console.log 的值工作得很好。只有在组件中显示时它们才不会显​​示。

从 UI:使用 test/test 登录。搜索一本书。添加到收藏。单击直通到详细信息页面。有一个播放和暂停按钮。页面上显示的是我从 StackOverflow 上找到的解决方案中尝试过的三种计时器变体。计时器从 6 秒开始倒计时至零。播放被点击计时器开始。暂停点击计时器停止,直到再次点击播放。在显示页面上,发出的值没有倒计时。控制台打开时,它会倒计时发出的值。

定时器由 core/services/pomo-timer.ts 处理

   startTimer() {

    const resumeButton = document.getElementById('resume');
    const pauseButton = document.getElementById('pause');
    const resetButton = document.getElementById('reset');
    const interval$: any = interval(1000).pipe(mapTo(-1));
    const pause$ = fromEvent(pauseButton, 'click').pipe(mapTo(false));
    const resume$ = fromEvent(resumeButton, 'click').pipe(mapTo(true));

   const timer$ = merge(pause$, resume$).pipe(
    startWith(interval$),
     switchMap(val => (val ? interval$ : empty())),
     scan((acc, curr) => (curr ? curr + acc : acc), 
     this.countdownSeconds$),
      takeWhile(v => v >= 0),
      )
     .subscribe(
      val => { this.timeRemaining = val; 
               console.log(this.timeRemaining); 
       },
        val => { this.checkTime.emit(val); },
        () => {
         this.resetTimer();
        });
       }

显示由 app/books/components/book-detail.ts

处理
export class BookDetailComponent {

  @Input() simpleObservable: number;
  @Input() seconds: string;
  @Input() timeRemaining: number;
  @Input() timerSubscription: Subscription;
  @Input() book: Book;
  @Input() inCollection: boolean;
  @Output() add = new EventEmitter<Book>();
  @Output() remove = new EventEmitter<Book>();
  @Output() resumeClicked = new EventEmitter();
  @Output() checkTime: EventEmitter<number> = new EventEmitter();

get id() {
 return this.book.id;
}

get title() {
 return this.book.volumeInfo.title;
 }

get subtitle() {
  return this.book.volumeInfo.subtitle;
}

get description() {
 return this.book.volumeInfo.description;
}

get thumbnail() {
 return (
   this.book.volumeInfo.imageLinks &&
   this.book.volumeInfo.imageLinks.smallThumbnail
  );
}

get time() {
  return this.timeRemaining;
 }
resumeCommand(action: any) {
  this.resumeClicked.emit(action);
 }
}

与计时器服务的通信由:app/books/containers/selected-book-page.ts

@Component({
  selector: 'bc-selected-book-page',
  changeDetection: ChangeDetectionStrategy.OnPush,
 template: `
   <bc-book-detail
    [book]="book$ | async"
    [inCollection]="isSelectedBookInCollection$ | async"
    [timeRemaining]="this.pomoTimerService.timeRemaining"
    [simpleObservable]="this.simpleObservable | async"
    [seconds]="this.pomoTimerService.timeRemaining"
    (checkTime)="checkCurrentTime($event)"
    (add)="addToCollection($event)"
    (remove)="removeFromCollection($event)"
    (resumeClicked)="resumeClicked($event)"
    (resumeClicked)="resumeClicked($event)"
    (reset)="resumeClicked($event)">
   </bc-book-detail>
  `,
  })
  export class SelectedBookPageComponent implements OnInit {
   book$: Observable<Book>;
   isSelectedBookInCollection$: Observable<boolean>;
   timeRemaining: any;
  private timerSubscription: Subscription;
  timerSource = new Subject<any>();
  simpleObservable;
  countDown: any;
  counter: number;
  seconds: string;
  private subscription: Subscription;
  checkTime;

 constructor(public pomoTimerService: PomoTimerService, private store: 
   Store<fromBooks.State>) {
   this.book$ = store.pipe(select(fromBooks.getSelectedBook));
   this.isSelectedBookInCollection$ = store.pipe(
   select(fromBooks.isSelectedBookInCollection)
  );
 }

ngOnInit(): void {
  this.pomoTimerService.pomoCount$ = 0;
  this.pomoTimerService.pomosCompleted$ = 0;
   this.pomoTimerService.pomoTitle$ = 'Time to Work';
   this.pomoTimerService.initTimer();
 }

addToCollection(book: Book) {
 this.store.dispatch(new collection.AddBook(book));
 }

 removeFromCollection(book: Book) {
  this.store.dispatch(new collection.RemoveBook(book));
  }

resumeClicked(event) {
  console.log(event);
  console.log(event.target);
  console.log(event.srcElement);
  console.log(event.type);
  console.log(event.currentTarget.attributes.name.nodeValue);
  console.log(event.currentTarget.attributes.id.nodeValue);
   if (event.currentTarget.attributes.id.nodeValue === 'resume' && 
    !this.pomoTimerService.timerStarted) {
    this.pomoTimerService.timerStarted = true;
    this.pomoTimerService.startTimer();
    }
   }

checkCurrentTime(event) {
  this.counter = event;
 }
}

pomo-timer.ts 正在通过this.remainingTime 输出计时器 您可能能够提供的任何帮助将不胜感激。我已经尝试了所有在 Stackoverflow 上找到的甚至远程相关的示例。非常感谢。

【问题讨论】:

  • 您需要提供最低限度的minimal reproducible example这里请。
  • @SurajRao 来自 UI:使用 test/test 登录。搜索一本书。添加到收藏。单击直通到详细信息页面。有一个播放和暂停按钮。页面上显示的是我从 StackOverflow 上找到的解决方案中尝试过的三种计时器变体。计时器从 6 秒开始倒计时至零。播放被点击计时器开始。暂停点击计时器停止,直到再次点击播放。在显示页面上,发出的值没有倒计时。控制台打开时,它会倒计时发出的值。我现在将在原始帖子中添加更多详细信息。
  • @suraj-rao 我在之前的评论中添加了上述步骤,并在原始帖子中添加了更多代码 sn-ps。
  • 我在上面添加了@SurajRao 重新创建的步骤。我已使用代码 sn-ps 在原始帖子中添加了更多详细信息。
  • 似乎没有检测到来自提供者的更改。您可能需要在您的剩余秒数上使用诸如 behaviorSubject 之类的东西并在容器中订阅

标签: angular rxjs ngrx


【解决方案1】:

我设法获得了一个有效的计时器服务。

我会在代码中重构很多内容,但在这里我提出了使其与现有应用程序结构一起工作所需的最少分类。

我应用的基本原则是:

  1. 异步订阅
    该服务会随着时间的推移产生值,因此应该在组件中以可观察的形式订阅,最好使用 async 管道,以便 Angular 自动清理订阅。

  2. 缓冲内部 observable
    使用Subject 作为 timer$ 和它的消费组件之间的缓冲区。这样即使在 timer$ 初始化之前,组件也始终可以看到有效的 observable。

  3. 使用 ViewChild 访问按钮
    不要访问带有document.getElementById() 的按钮,因为运行此行时文档可能尚未准备好。改用 Angular 的 @ViewChild,并在初始化时将元素传递给服务。

这些是我制作的模组。为简洁起见,我已经删除了未更改的块,希望有足够的细节供您进行更改。

PomoTimerService 模组

// imports as before, plus
import { tap } from 'rxjs/operators';

@Injectable()
export class PomoTimerService {

  timerSource$ = new Subject<any>(); // added '$' to this property, for clarity
  // other properties same as before

  private buttons; // to receive button references passed in

  // Create a new version of init, which is called once and receives the buttons
  initTimer (buttons) {  
    this.buttons = buttons;
    this.initTimerParameters();
  }

  // Renamed the original initTimer() method to initTimerParamters, 
  // as it is called on true init and also in reset    
  initTimerParameters() {
    // same statements as original initTimer() method
  }

  startTimer() {
    this.timerStarted = true;  // moved from component
    const interval$: any = interval(1000).pipe(mapTo(-1));
    const pause$ = fromEvent(this.buttons.pauseButton.nativeElement, 'click').pipe(mapTo(false));
    const resume$ = fromEvent(this.buttons.resumeButton.nativeElement, 'click').pipe(mapTo(true));

    const timer$ = merge(pause$, resume$).pipe(
      startWith(true),  // previously startWith(interval$), but that looks suspect
      switchMap(val => (val ? interval$ : empty())),
      scan((acc, curr) => (curr ? curr + acc : acc), this.countdownSeconds$),
      takeWhile(v => v >= 0),
      tap(val => console.log('timeRemaining', val)),  // use tap (not subscribe) to monitor on console 
      tap(val => {  // resetting this.timerStarted is a 'side-effect', best done with tap operator rather than finally callback of subscribe
        if (val === 0) {
          this.timerStarted = false;
        }
      }),
    );

    timer$.subscribe(val => this.timerSource$.next(val))  // send values to Subject
  }

  resetTimer() {
    this.initTimerParameters();  // was calling this.initTimer()
  }
}

book-detail.ts - 模板模组

通过服务的Subjectasync 管道使用计时器值。
将模板变量添加到按钮以用于@ViewChild 属性。

@Component({
  selector: 'bc-book-detail',
  template: `
    <mat-card *ngIf="book">
      ...
      <mat-card-subtitle>Original {{ timerService.timerSource$ | async }} 
      </mat-card-subtitle>
      ...
      <button #resume id="resume" ...</button>
      <button #pause id="pause" ...</button>
      <button #reset id="reset" ...</button>
      </mat-card-actions>
    </mat-card>

  `,

book-detail.ts - javacript 模组

通过构造函数注入器引入服务,调用 initTimer()。 使用@ViewChild 将按钮发送到服务。

export class BookDetailComponent implements AfterViewInit {

  // @Inputs and @Outputs as previously defined

  constructor(public timerService: PomoTimerService) {}

  @ViewChild('resume', {read: ElementRef}) resumeButton;
  @ViewChild('pause', {read: ElementRef}) pauseButton;
  @ViewChild('reset', {read: ElementRef}) resetButton;

  ngAfterViewInit() {
    const buttons = {
      resumeButton: this.resumeButton,
      pauseButton: this.pauseButton,
      resetButton: this.resetButton
    };
    this.timerService.initTimer(buttons);
  }

【讨论】:

  • 感谢 Richard 提供的解决方案。至少我在正确的轨道上,因为我尝试过这样的事情但无法让它发挥作用。不幸的是,我仍然无法让它工作。你将什么作为@Input 传递给组件?此时,我什至没有看到计时器的 console.log。
  • 我现在开始工作了!但暂停不起作用。即使按下暂停,计时器也会继续发出值。
  • 很高兴看到您在这方面取得了进展。暂停按钮与开始/恢复按钮的工作方式相同,因此听起来您的代码中某处有错字。它当然适用于我的测试系统。
  • 您有什么方法可以与我分享您为实现此功能所做的代码。我仍然无法让计时器暂停。没有错别字。我实施了你推荐的一切。它不起作用。我一直试图让这个工作好几天了,但我束手无策。我会很感激。谢谢
  • 这是StackBlitz
猜你喜欢
  • 2016-12-30
  • 1970-01-01
  • 1970-01-01
  • 2018-10-08
  • 2019-12-04
  • 1970-01-01
  • 1970-01-01
  • 2016-12-28
  • 2018-10-21
相关资源
最近更新 更多