如何在不刷新页面的情况下启动计时器 (Rails/JavaScript)?

How to start a timer without page refresh (Rails/JavaScript)?

我有一个名为 'Deal' 的模型,它具有 start_atend_at 属性。我已经使用 hotwire/stimulus JS 实现了一个倒数计时器。

  1. 当交易开始时(开始日期是过去的日期,结束日期是未来的日期),将显示显示剩余交易时间的倒数计时器。例如,剩余交易时间:2 小时 4 分 30 秒,依此类推。它将减少 1 秒。
  2. 如果交易尚未开始(开始日期是未来),页面将显示“交易将于#{datetime} 开始”。

但是,如果交易同时开始,用户需要刷新他们当前所在的页面才能看到计时器(即从“交易将于 #{datetime} 开始”过渡到倒计时) ).我想知道在不刷新页面的情况下启动计时器的最佳方法是什么。谢谢

管理 'timer' 每 X 毫秒执行一次 运行 某些功能的方法是通过浏览器的 setInterval function

这个函数可以像这样使用 - const intervalID = setInterval(myCallback, 500); - 其中 myCallback 是每 500 毫秒 运行 的函数。

计时器可以是 'cancelled',方法是调用 clearInterval 并为其提供作为 setInterval.

结果创建的间隔 ID

示例HTML

  • 这里我们有一个基本的 HTMl 结构,我们在其中设置我们的控制器 timer 并设置 from/to 时间以及保存基于三种状态的消息的目标。
  • 这三种状态分别是'before'、'during'(当当前时间在两个时间之间时)和'after'.
<section class="prose m-5">
  <div
    data-controller="timer"
    data-timer-from-value="2022-03-08T10:41:32.111Z"
    data-timer-to-value="2022-03-09T11:10:32.111Z"
  >
    <div style="display: none" data-timer-target="before">
      Deal will start on <time data-timer-target="fromTime"></time>
    </div>
    <div style="display: none" data-timer-target="during">
      Deal is active <time data-timer-target="toTimeRelative"></time>
    </div>
    <div style="display: none" data-timer-target="after">
      Deal ended on <time data-timer-target="toTime"></time>
    </div>
  </div>
</section>

示例刺激控制器

  • timerController 接受 tofrom 时间作为字符串(最好使用 ISO 字符串,并记住 time-zones 的细微差别可能很复杂)。
  • 当控制器连接时,我们做三件事; 1. 设置一个计时器到运行 this.update 每X 毫秒并将计时器ID 放在class 上以便稍后清除为this._timer。 2. 设置时间值(消息传递的内部时间标签)。 3. 运行 this.update 方法初始时间。
  • this.getTimeData 解析 from/to 日期时间字符串并进行一些基本验证,它还 returns 这些日期对象以及一个 status 字符串,它将是 BEFORE/DURING/AFTER.
  • this.update - 此 shows/hides 基于已解决状态的相关消息部分。
import { Controller } from '@hotwired/stimulus';

const BEFORE = 'BEFORE';
const DURING = 'DURING';
const AFTER = 'AFTER';

export default class extends Controller {
  static values = {
    interval: { default: 500, type: Number },
    locale: { default: 'en-GB', type: String },
    from: String,
    to: String,
  };

  static targets = [
    'before',
    'during',
    'after',
    'fromTime',
    'toTime',
    'toTimeRelative',
  ];

  connect() {
    this._timer = setInterval(() => {
      this.update();
    }, this.intervalValue);

    this.setTimeValues();
    this.update();
  }

  getTimeData() {
    const from = this.hasFromValue && new Date(this.fromValue);
    const to = this.hasToValue && new Date(this.toValue);

    if (!from || !to) return;
    if (from > to) {
      throw new Error('From time must be after to time.');
    }

    const now = new Date();

    const status = (() => {
      if (now < from) return BEFORE;

      if (now >= from && now <= to) return DURING;

      return AFTER;
    })();

    return { from, to, now, status };
  }

  setTimeValues() {
    const { from, to, now } = this.getTimeData();
    const locale = this.localeValue;

    const formatter = new Intl.DateTimeFormat(locale, {
      dateStyle: 'short',
      timeStyle: 'short',
    });

    this.fromTimeTargets.forEach((element) => {
      element.setAttribute('datetime', from);
      element.innerText = formatter.format(from);
    });

    this.toTimeTargets.forEach((element) => {
      element.setAttribute('datetime', to);
      element.innerText = formatter.format(to);
    });

    const relativeFormatter = new Intl.RelativeTimeFormat(locale, {
      numeric: 'auto',
    });

    this.toTimeRelativeTargets.forEach((element) => {
      element.setAttribute('datetime', to);
      element.innerText = relativeFormatter.format(
        Math.round((to - now) / 1000),
        'seconds'
      );
    });
  }

  update() {
    const { status } = this.getTimeData();

    [
      [BEFORE, this.beforeTarget],
      [DURING, this.duringTarget],
      [AFTER, this.afterTarget],
    ].forEach(([key, element]) => {
      if (key === status) {
        element.style.removeProperty('display');
      } else {
        element.style.setProperty('display', 'none');
      }
    });

    this.setTimeValues();

    if (status === AFTER) {
      this.stopTimer();
    }
  }

  stopTimer() {
    const timer = this._timer;

    if (!timer) return;

    clearInterval(timer);
  }

  disconnect() {
    // ensure we clean up so the timer is not running if the element gets removed
    this.stopTimer();
  }
}