feat: 🎸 implement live relative datetime component (#1080)
This commit is contained in:
1 parent
de6416efbd
commit
a7b3da2231
9 files changed
+244
-1
No files matched your search
@@ -0,0 +1 @@
|
||||
{{this.formatted}}
|
||||
@@ -0,0 +1,100 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
const MILLISECOND = 1;
|
||||
const SECOND = MILLISECOND * 1000;
|
||||
const MINUTE = SECOND * 60;
|
||||
const HOUR = MINUTE * 60;
|
||||
const DAY = HOUR * 24;
|
||||
const MONTH = DAY * 31;
|
||||
const YEAR = MONTH * 12;
|
||||
|
||||
// common time intervals in milliseconds
|
||||
const unitMappings = [
|
||||
{
|
||||
unit: 'second',
|
||||
value: SECOND,
|
||||
},
|
||||
{
|
||||
unit: 'minute',
|
||||
value: MINUTE,
|
||||
},
|
||||
{
|
||||
unit: 'hour',
|
||||
value: HOUR,
|
||||
},
|
||||
{
|
||||
unit: 'day',
|
||||
value: DAY,
|
||||
},
|
||||
{
|
||||
unit: 'month',
|
||||
value: MONTH,
|
||||
},
|
||||
{
|
||||
unit: 'year',
|
||||
value: YEAR,
|
||||
},
|
||||
];
|
||||
|
||||
export default class RelativeDatetimeLiveComponent extends Component {
|
||||
// =services
|
||||
|
||||
@service clockTick;
|
||||
@service intl;
|
||||
|
||||
// =properties
|
||||
|
||||
/**
|
||||
* The difference in milliseconds between the date argument and the
|
||||
* current time.
|
||||
* @type {number}
|
||||
*/
|
||||
get delta() {
|
||||
return this.args.date.valueOf() - this.clockTick.now;
|
||||
}
|
||||
|
||||
/**
|
||||
* The greatest meaningful unit is the unit which delta is equal to or
|
||||
* greater than. For example, the unit of a delta equivalent of 59 seconds is
|
||||
* 'second' while 60 seconds is 'minute', 36 hours is 'day', etc.
|
||||
* @type {object}
|
||||
*/
|
||||
get greatestMeaningfulUnit() {
|
||||
return unitMappings.reduce((currentValue, previousValue) => {
|
||||
return Math.abs(this.delta) >= previousValue.value
|
||||
? previousValue
|
||||
: currentValue;
|
||||
}, unitMappings[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The delta value scaled to the greatest meaningful unit. For example, if
|
||||
* the delta is 1000 ms, the scaled value is 1, which corresponds to
|
||||
* a GMU of 'second'. A delta of 60000 ms is also 1, corresponding to a GMU
|
||||
* of 'minute'.
|
||||
* @type {number}
|
||||
*/
|
||||
get scaledDelta() {
|
||||
if (this.delta < 0) {
|
||||
return Math.ceil(this.delta / this.greatestMeaningfulUnit.value);
|
||||
}
|
||||
return Math.floor(this.delta / this.greatestMeaningfulUnit.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A string representation of the greatest meaningful unit.
|
||||
* @type {string}
|
||||
*/
|
||||
get unit() {
|
||||
return this.greatestMeaningfulUnit.unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* A localizd relative datetime. E.g. "30 seconds ago".
|
||||
* @type {string}
|
||||
*/
|
||||
get formatted() {
|
||||
return this.intl.formatRelative(this.scaledDelta, { unit: this.unit });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import Service from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { next } from '@ember/runloop';
|
||||
|
||||
export default class ClockTickService extends Service {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* The current datetime. This is updated once every `interval` while the
|
||||
* service is running (1s by default).
|
||||
* @type {number}
|
||||
*/
|
||||
@tracked now = Date.now();
|
||||
|
||||
/**
|
||||
* The frequency to update `now`, in milliseconds.
|
||||
* @type {number}
|
||||
*/
|
||||
frequency = 1000;
|
||||
|
||||
/**
|
||||
* The timer interval ID
|
||||
*/
|
||||
#timer;
|
||||
|
||||
/**
|
||||
* This service is enabled only in non-test environments.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get enabled() {
|
||||
const config = getOwner(this).resolveRegistration('config:environment');
|
||||
return config.environment !== 'test';
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Starts the timer, if enabled.
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
if (this.enabled) this._start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the timer.
|
||||
*/
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this._stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the timer interval, calling `_tick()` every
|
||||
* `frequency` milliseconds.
|
||||
*/
|
||||
_start() {
|
||||
const self = this;
|
||||
this.#timer = setInterval(function () {
|
||||
self._tick();
|
||||
}, this.frequency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the timer interval.
|
||||
*/
|
||||
_stop() {
|
||||
clearInterval(this.#timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the value of `now` within the next runloop.
|
||||
*/
|
||||
_tick() {
|
||||
next(() => (this.now = Date.now()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/components/relative-datetime-live';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/services/clock-tick';
|
||||
@@ -0,0 +1,12 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ApplicationController extends Controller {
|
||||
// =services
|
||||
|
||||
@service clockTick;
|
||||
|
||||
// =properties
|
||||
|
||||
now = new Date();
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
<h2 id="title">Welcome to Ember</h2>
|
||||
|
||||
{{t "errors.401.title"}}
|
||||
<RelativeDatetimeLive @date={{this.now}} />
|
||||
@@ -0,0 +1,38 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | relative-datetime-live', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders a relative datetime', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
this.date = now - 10 * 1000;
|
||||
await render(hbs`<RelativeDatetimeLive @date={{this.date}} />`);
|
||||
assert.dom(this.element).hasText('10 seconds ago');
|
||||
|
||||
this.date = now - 10 * 60 * 1000;
|
||||
await render(hbs`<RelativeDatetimeLive @date={{this.date}} />`);
|
||||
assert.dom(this.element).hasText('10 minutes ago');
|
||||
|
||||
this.date = now - 10 * 60 * 60 * 1000;
|
||||
await render(hbs`<RelativeDatetimeLive @date={{this.date}} />`);
|
||||
assert.dom(this.element).hasText('10 hours ago');
|
||||
|
||||
this.date = now - 10 * 24 * 60 * 60 * 1000;
|
||||
await render(hbs`<RelativeDatetimeLive @date={{this.date}} />`);
|
||||
assert.dom(this.element).hasText('10 days ago');
|
||||
|
||||
this.date = now - 10 * 31 * 24 * 60 * 60 * 1000;
|
||||
await render(hbs`<RelativeDatetimeLive @date={{this.date}} />`);
|
||||
assert.dom(this.element).hasText('10 months ago');
|
||||
|
||||
this.date = now - 10 * 12 * 31 * 24 * 60 * 60 * 1000;
|
||||
await render(hbs`<RelativeDatetimeLive @date={{this.date}} />`);
|
||||
assert.dom(this.element).hasText('10 years ago');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Service | clock-tick', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let service = this.owner.lookup('service:clock-tick');
|
||||
assert.ok(service);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user