feat: 🎸 implement live relative datetime component (#1080)

This commit is contained in:
Randall Morey authored and GitHub committed 2022-04-28 16:23:44 -04:00
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 });
}
}
+78
View File
@@ -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';
+1
View File
@@ -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);
});
});