Skip to content

Commit fc4f404

Browse files
Merge pull request #143 from universal-ember/interval
New utilities: `Interval`, `Seconds`, `Duration`, imprtable from `reactiveweb/interval`
2 parents ed0c22b + 1ecfbc0 commit fc4f404

File tree

2 files changed

+199
-0
lines changed

2 files changed

+199
-0
lines changed

reactiveweb/src/interval.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { cell, resource, resourceFactory } from 'ember-resources';
2+
3+
export interface Options<State, Value> {
4+
create: () => State;
5+
update: (state: State) => void;
6+
read: (state: State) => Value;
7+
}
8+
9+
/**
10+
* Utility for live-updating data based on some interval.
11+
* Can be used for keeping track of durations, time-elapsed, etc.
12+
*
13+
* Defaults to updating every 1 second.
14+
* Options requires specifying how to create, update, and read the state.
15+
*/
16+
export function Interval<State, Value>(ms = 1000, options: Options<State, Value>) {
17+
return resource(({ on }) => {
18+
const value = options.create();
19+
const interval = setInterval(() => {
20+
options.update(value);
21+
}, ms);
22+
23+
on.cleanup(() => {
24+
clearInterval(interval);
25+
});
26+
27+
return () => options.read(value);
28+
});
29+
}
30+
31+
const counterOptions: Options<ReturnType<typeof cell<number>>, number> = {
32+
create: () => cell(0),
33+
update: (x) => void x.current++,
34+
read: (x) => x.current,
35+
};
36+
37+
/**
38+
* Returns a live-updating count of seconds passed since initial rendered.
39+
* Always returns an integer.
40+
* Updates every 1 second.
41+
*/
42+
export function Seconds() {
43+
return Interval(1000, counterOptions);
44+
}
45+
46+
const durationOptions: Options<{ start: number; last: ReturnType<typeof cell<number>> }, number> = {
47+
create: () => ({ start: Date.now(), last: cell(Date.now()) }),
48+
update: (x) => void (x.last.current = Date.now()),
49+
read: (x) => x.last.current - x.start,
50+
};
51+
52+
/**
53+
* Returns a live-updating duration since initial render.
54+
* Measured in milliseconds.
55+
*
56+
* By default updates every 1 second.
57+
*
58+
* Useful combined with
59+
* [Temporal.Duration](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration)
60+
*/
61+
export function Duration(ms = 1000) {
62+
return Interval(ms, durationOptions);
63+
}
64+
65+
resourceFactory(Interval);
66+
resourceFactory(Seconds);
67+
resourceFactory(Duration);
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import Component from '@glimmer/component';
2+
import { tracked } from '@glimmer/tracking';
3+
import { find, render } from '@ember/test-helpers';
4+
import { module, test } from 'qunit';
5+
import { setupRenderingTest } from 'ember-qunit';
6+
7+
import { cell, use } from 'ember-resources';
8+
import { Duration, Interval, Seconds } from 'reactiveweb/interval';
9+
10+
function timeout(ms: number) {
11+
return new Promise((resolve) => setTimeout(resolve, ms));
12+
}
13+
14+
module('Interval | js', function (hooks) {
15+
setupRenderingTest(hooks);
16+
17+
test('it works', async function (assert) {
18+
class Test extends Component {
19+
@tracked foo = 1;
20+
@use value = Interval(50, {
21+
create: () => this.foo,
22+
update: (x) => (this.foo = x * 3),
23+
read: (x) => x,
24+
});
25+
26+
<template>{{this.value}}</template>
27+
}
28+
29+
await render(Test);
30+
assert.dom().hasText('1');
31+
32+
await timeout(75);
33+
assert.dom().hasText('3');
34+
35+
await timeout(50);
36+
assert.dom().hasText('9');
37+
});
38+
});
39+
40+
module('Interval | rendering', function (hooks) {
41+
setupRenderingTest(hooks);
42+
43+
test('it works', async function (assert) {
44+
const config = {
45+
create: () => cell('hello'),
46+
update: (x: ReturnType<typeof cell<string>>) => (x.current += '!'),
47+
read: (x: ReturnType<typeof cell<string>>) => x.current,
48+
};
49+
50+
await render(<template>{{Interval 50 config}}</template>);
51+
52+
assert.dom().hasText('hello');
53+
await timeout(75);
54+
55+
assert.dom().hasText('hello!');
56+
57+
await timeout(50);
58+
assert.dom().hasText('hello!!');
59+
});
60+
});
61+
62+
module('Seconds | js', function (hooks) {
63+
setupRenderingTest(hooks);
64+
65+
test('it works', async function (assert) {
66+
class Test extends Component {
67+
@use value = Seconds();
68+
69+
<template>{{this.value}}</template>
70+
}
71+
72+
await render(Test);
73+
assert.dom().hasText('0');
74+
75+
await timeout(1050);
76+
assert.dom().hasText('1');
77+
});
78+
});
79+
80+
module('Seconds | rendering', function (hooks) {
81+
setupRenderingTest(hooks);
82+
83+
test('it works', async function (assert) {
84+
await render(<template>{{Seconds}}</template>);
85+
86+
assert.dom().hasText('0');
87+
88+
await timeout(1050);
89+
assert.dom().hasText('1');
90+
});
91+
});
92+
93+
module('Duration | js', function (hooks) {
94+
setupRenderingTest(hooks);
95+
96+
const text = () => find('out')?.textContent;
97+
98+
test('it works', async function (assert) {
99+
class Test extends Component {
100+
@use value = Duration(10);
101+
102+
<template>
103+
<out>{{this.value}}</out>
104+
</template>
105+
}
106+
107+
await render(Test);
108+
assert.strictEqual(text(), '0', `expecting ${text()} to be 0`);
109+
110+
await timeout(150);
111+
assert.notStrictEqual(text(), '0', `expecting ${text()} not to be 0`);
112+
});
113+
});
114+
115+
module('Duration | rendering', function (hooks) {
116+
setupRenderingTest(hooks);
117+
118+
const text = () => find('out')?.textContent;
119+
120+
test('it works', async function (assert) {
121+
await render(
122+
<template>
123+
<out>{{Duration 10}}</out>
124+
</template>
125+
);
126+
127+
assert.strictEqual(text(), '0', `expecting ${text()} to be 0`);
128+
129+
await timeout(150);
130+
assert.notStrictEqual(text(), '0', `expecting ${text()} not to be 0`);
131+
});
132+
});

0 commit comments

Comments
 (0)