diff --git a/package.json b/package.json index d7af8294d8..286fd8d670 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "eslint-plugin-vue": "^5.0.0", "html-loader": "^0.5.5", "lint-staged": "^9.5.0", + "mockdate": "^2.0.5", "prettier": "^1.19.1", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/src/compositions/readme.md b/src/compositions/readme.md index 8c8877858e..705063ac5d 100644 --- a/src/compositions/readme.md +++ b/src/compositions/readme.md @@ -1,10 +1,11 @@ # Compositions -Compositions are reusable snippets of functionality that can be used in Vue components. +Compositions are reusable pieces of logic that can be used inside Vue components (Composition API required). ## Table of Contents * [Event Listener](#event-listener) +* [Time from Now](#time-from-now) * [Window Size](#window-size) ## Event Listener @@ -32,6 +33,27 @@ export default createComponent({ }); ``` + +## Time from Now + +Returns ref string time from current datetime based on date-fns formatDistance. + +### Usage + +```js +import { createComponent } from '@vue/composition-api'; +import useTimeFromNow from '@/compositions/time-from-now'; + +export default createComponent({ + setup() { + const date = new Date('2020-01-01T13:55'); + const timeFromNow = useTimeFromNow(date); + } +}); +``` + +The composition accepts an optional second parameter that controls how often the value is update. You can set this to `0` if you don't want the value to update at all. + ## Window Size Returns a `ref` of `width` and `height` of the current window size. Updates the value on window resizes. diff --git a/src/compositions/time-from-now.test.ts b/src/compositions/time-from-now.test.ts new file mode 100644 index 0000000000..0129cb03b3 --- /dev/null +++ b/src/compositions/time-from-now.test.ts @@ -0,0 +1,90 @@ +import { Ref } from '@vue/composition-api'; +import useTimeFromNow from './time-from-now'; +import mountComposition from '../../.jest/mount-composition'; +import mockdate from 'mockdate'; + +describe('Compositions / Event Listener', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + mockdate.reset(); + }); + + it('Formats the date relative', () => { + const now = new Date(); + let timeAgo: Ref; + let timeAhead: Ref; + + mountComposition(() => { + timeAgo = useTimeFromNow(new Date(now.getTime() - 5 * 60 * 1000)); + timeAhead = useTimeFromNow(new Date(now.getTime() + 5 * 60 * 1000)); + }); + + expect(timeAgo!.value).toBe('5 minutes ago'); + expect(timeAhead!.value).toBe('in 5 minutes'); + }); + + it('Updates the ref every minute by default', () => { + mockdate.set('2020-01-01T12:00:00'); + const now = new Date(); + + const component = mountComposition(() => { + const timeAgo = useTimeFromNow(new Date(now.getTime() - 5 * 60 * 1000)); + return { timeAgo }; + }); + + expect((component.vm as any).timeAgo).toBe('5 minutes ago'); + + mockdate.set('2020-01-01T12:01:00'); + jest.runTimersToTime(60000); + + expect(setInterval).toHaveBeenCalledTimes(1); + + expect((component.vm as any).timeAgo).toBe('6 minutes ago'); + }); + + it('Does not automatically update if 0 is passed for autoUpdate param', () => { + mockdate.set('2020-01-01T12:00:00'); + const now = new Date(); + + const component = mountComposition(() => { + const timeAgo = useTimeFromNow(new Date(now.getTime() - 5 * 60 * 1000), 0); + return { timeAgo }; + }); + + expect((component.vm as any).timeAgo).toBe('5 minutes ago'); + + mockdate.set('2020-01-01T12:01:00'); + jest.runTimersToTime(60000); + + expect(setInterval).toHaveBeenCalledTimes(0); + + expect((component.vm as any).timeAgo).toBe('5 minutes ago'); + }); + + it('Clears the interval when the component is unmounted', () => { + mockdate.set('2020-01-01T12:00:00'); + const now = new Date(); + + const component = mountComposition(() => { + const timeAgo = useTimeFromNow(new Date(now.getTime() - 5 * 60 * 1000)); + return { timeAgo }; + }); + + expect((component.vm as any).timeAgo).toBe('5 minutes ago'); + + mockdate.set('2020-01-01T12:01:00'); + jest.runTimersToTime(60000); + + expect(setInterval).toHaveBeenCalledTimes(1); + + component.destroy(); + + mockdate.set('2020-01-01T12:01:00'); + jest.runTimersToTime(60000); + + expect(setInterval).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/compositions/time-from-now.ts b/src/compositions/time-from-now.ts new file mode 100644 index 0000000000..45d9584b62 --- /dev/null +++ b/src/compositions/time-from-now.ts @@ -0,0 +1,26 @@ +import { onMounted, onUnmounted, ref } from '@vue/composition-api'; +import formatDistance from 'date-fns/formatDistance'; + +export default function useFormatDistance(date: Date | number, autoUpdate: number = 60000) { + let interval: number; + + const formatOptions = { + addSuffix: true + }; + + const formattedDate = ref(formatDistance(date, new Date(), formatOptions)); + + if (autoUpdate !== 0) { + onMounted(() => { + interval = setInterval(() => { + formattedDate.value = formatDistance(date, new Date(), formatOptions); + }, autoUpdate); + }); + + onUnmounted(() => { + clearInterval(interval); + }); + } + + return formattedDate; +} diff --git a/yarn.lock b/yarn.lock index 7c830031f5..eeaca11444 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9384,6 +9384,11 @@ mkdirp@0.5.1, mkdirp@0.x, mkdirp@^0.5.1, mkdirp@~0.5.1, mkdirp@~0.5.x: dependencies: minimist "0.0.8" +mockdate@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-2.0.5.tgz#70c6abf9ed4b2dae65c81dfc170dd1a5cec53620" + integrity sha512-ST0PnThzWKcgSLyc+ugLVql45PvESt3Ul/wrdV/OPc/6Pr8dbLAIJsN1cIp41FLzbN+srVTNIRn+5Cju0nyV6A== + moment@^2.18.1: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" @@ -10317,7 +10322,7 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pinia@^0.0.5: +pinia@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/pinia/-/pinia-0.0.5.tgz#e95fde7c3de5cdef1d4bd71fa0d9c3110862c641" integrity sha512-EF3LpCYsA1VSm9EeCK87UHA66NFM/7++D0qnWTzSPgQ+UKoUQytWsXat777bj9WDzjNELxKlyP3Ifv44IURi2g==