├── .eslintignore ├── .gitignore ├── .npmignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── .babelrc ├── tests ├── ssr │ ├── index.js │ ├── index.html │ ├── app.jsx │ └── server.js ├── objects.js └── index.jsx ├── .travis.yml ├── .editorconfig ├── src ├── objects.js └── index.jsx ├── LICENSE ├── webpack.config.js ├── stories └── index.js ├── dist └── index.d.ts ├── package.json ├── .eslintrc └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/webpack.config*.js 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .cache 3 | node_modules 4 | stories/build 5 | coverage 6 | *.log 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .editorconfig 3 | .travis.yml 4 | .storybook 5 | stories 6 | webpack.config.js 7 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import 'storybook-addon-jsx/register' 2 | import '@storybook/addon-knobs/register'; 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/ssr/index.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | ignore: [/(node_modules)/], 3 | presets: ['@babel/preset-env', '@babel/preset-react'] 4 | }); 5 | 6 | require('./server'); 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.4.0" 4 | - "8.0.0" 5 | before_install: 6 | - export TZ=America/New_York 7 | - date 8 | script: 9 | - npm run travis-test 10 | -------------------------------------------------------------------------------- /tests/ssr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Moment 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{js,jsx,css,scss,less,coffee,yml,json}] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, setAddon } from '@storybook/react'; 2 | import JSXAddon from 'storybook-addon-jsx' 3 | 4 | setAddon(JSXAddon); 5 | 6 | function loadStories() { 7 | require('../stories'); 8 | } 9 | 10 | configure(loadStories, module); 11 | -------------------------------------------------------------------------------- /tests/ssr/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Moment from '../../dist/index'; 3 | 4 | /** 5 | * 6 | */ 7 | class App extends React.Component { 8 | /** 9 | * @returns {*} 10 | */ 11 | render() { 12 | return ( 13 |
14 | The time is now:  15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /tests/objects.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import * as objects from '../src/objects'; 3 | 4 | test('objectKeyFilter', () => { 5 | const propTypes = { 6 | className: PropTypes.string 7 | }; 8 | const props = { 9 | className: 'dp-item', 10 | title: 'foo' 11 | }; 12 | const result = objects.objectKeyFilter(props, propTypes); 13 | expect(result.className).toBe(undefined); 14 | expect(result.title).not.toBe(undefined); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 2 | // This is just the basic way to add additional webpack configurations. 3 | // For more information refer the docs: https://storybook.js.org/configurations/custom-webpack-config 4 | 5 | // IMPORTANT 6 | // When you add this file, we won't add the default configurations which is similar 7 | // to "React Create App". This only has babel loader to load JavaScript. 8 | 9 | module.exports = { 10 | plugins: [ 11 | // your custom plugins 12 | ], 13 | module: { 14 | rules: [ 15 | // add your custom loaders. 16 | ] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /tests/ssr/server.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import express from 'express'; 4 | import React from 'react'; 5 | import ReactDOMServer from 'react-dom/server'; 6 | import App from './app'; 7 | 8 | const PORT = 8081; 9 | const app = express(); 10 | const router = express.Router(); 11 | 12 | const serverRenderer = (req, res) => { 13 | fs.readFile(path.resolve('./index.html'), 'utf8', (err, data) => { 14 | if (err) { 15 | console.error(err); 16 | return res.status(500).send('An error occurred'); 17 | } 18 | 19 | return res.send( 20 | data.replace( 21 | '
', 22 | `
${ReactDOMServer.renderToString()}
` 23 | ) 24 | ); 25 | }); 26 | }; 27 | 28 | router.use('^/$', serverRenderer); 29 | app.use(router); 30 | app.listen(PORT, () => { 31 | console.log(`SSR running on port ${PORT}`); 32 | }); 33 | -------------------------------------------------------------------------------- /src/objects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Performs a key comparison between two objects, deleting from the first where 3 | * the keys exist in the second 4 | * 5 | * Can be used to remove unwanted component prop values. For example: 6 | * 7 | * ```jsx 8 | * render() { 9 | * const { children, className, ...props } = this.props; 10 | * 11 | * return ( 12 | *
16 | * {children} 17 | *
18 | * ) 19 | * } 20 | * ``` 21 | * 22 | * @param {Object} obj1 23 | * @param {Object} obj2 24 | * @returns {*} 25 | */ 26 | export function objectKeyFilter(obj1, obj2) { 27 | const obj2Keys = Object.keys(obj2); 28 | const newProps = Object.assign({}, obj1); 29 | Object.keys(newProps) 30 | .filter(key => obj2Keys.indexOf(key) !== -1) 31 | .forEach(key => delete newProps[key]); 32 | 33 | return newProps; 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Sean Hickey 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.jsx', 5 | output: { 6 | path: path.resolve(__dirname, 'dist'), 7 | filename: 'index.js', 8 | library: 'react-moment', 9 | libraryTarget: 'umd', 10 | globalObject: 'this' 11 | }, 12 | externals: [ 13 | { 14 | 'react': { 15 | root: 'react', 16 | commonjs2: 'react', 17 | commonjs: 'react', 18 | amd: 'react' 19 | }, 20 | 'moment': { 21 | root: 'moment', 22 | commonjs2: 'moment', 23 | commonjs: 'moment', 24 | amd: 'moment' 25 | } 26 | } 27 | ], 28 | module: { 29 | rules: [ 30 | { 31 | include: /\.json$/, 32 | loader: require.resolve('json-loader') 33 | }, 34 | { 35 | test: /\.jsx?$/, 36 | exclude: /(node_modules)/, 37 | loader: require.resolve('babel-loader'), 38 | } 39 | ] 40 | }, 41 | resolve: { 42 | extensions: ['.json', '.js', '.jsx'] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension */ 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { withKnobs, text, number } from '@storybook/addon-knobs'; 5 | import Moment from '../src/index'; 6 | 7 | Moment.globalLocale = 'fr'; 8 | 9 | storiesOf('Moment', module) 10 | .addDecorator(withKnobs) 11 | .addWithJSX('with default props', () => { 12 | return ( 13 | 14 | ); 15 | }) 16 | .addWithJSX('using the format prop', () => { 17 | return ( 18 | 19 | ); 20 | }) 21 | .addWithJSX('using the fromNow prop', () => { 22 | return ( 23 | 24 | ); 25 | }) 26 | .addWithJSX('using add and subtract props', () => { 27 | return ( 28 |
29 | 30 |
31 | 32 |
33 | ); 34 | }) 35 | .addWithJSX('using the unix prop', () => { 36 | return ( 37 | 38 | ); 39 | }) 40 | .addWithJSX('using filter prop', () => { 41 | return ( 42 | { 45 | return d.toUpperCase(); 46 | }} 47 | /> 48 | ); 49 | }) 50 | ; 51 | 52 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { Component, ComponentClass, SFC, CSSProperties } from 'react'; 3 | 4 | type elementTypes = string | SFC | ComponentClass; 5 | type subtractOrAddTypes = { 6 | years?: number, 7 | y?: number, 8 | quarters?: number, 9 | Q?: number, 10 | months?: number, 11 | M?: number, 12 | weeks?: number, 13 | w?: number, 14 | days?: number, 15 | d?: number, 16 | hours?: number, 17 | h?: number, 18 | minutes?: number, 19 | m?: number, 20 | seconds?: number, 21 | s?: number, 22 | milliseconds?: number, 23 | ms?: number 24 | }; 25 | type dateTypes = string|number|Array|object; 26 | type calendarTypes = boolean|object; 27 | 28 | export interface MomentProps { 29 | element?: elementTypes, 30 | date?: dateTypes, 31 | parse?: string | Array, 32 | format?: string, 33 | ago?: boolean, 34 | fromNow?: boolean, 35 | fromNowDuring?: number, 36 | from?: dateTypes, 37 | toNow?: boolean, 38 | to?: dateTypes, 39 | calendar?: calendarTypes, 40 | diff?: dateTypes, 41 | duration?: dateTypes, 42 | durationFromNow?: boolean, 43 | unit?: string, 44 | decimal?: boolean, 45 | unix?: boolean, 46 | utc?: boolean, 47 | local?: boolean, 48 | tz?: string, 49 | locale?: string, 50 | interval?: number, 51 | withTitle?: boolean, 52 | titleFormat?: string, 53 | subtract?: subtractOrAddTypes, 54 | add?: subtractOrAddTypes, 55 | children?: string | number | Date | moment.Moment, 56 | style?: CSSProperties, 57 | className?: string, 58 | filter?: (date: string) => string, 59 | onChange?: (content:any) => any 60 | } 61 | 62 | declare class Moment extends Component { 63 | constructor(props:MomentProps); 64 | public static globalMoment: Function; 65 | public static globalLocale: string; 66 | public static globalLocal: boolean; 67 | public static globalFormat: string; 68 | public static globalParse: string; 69 | public static globalTimezone: string; 70 | public static globalElement: any; 71 | public static globalFilter: Function; 72 | public static startPooledTimer(interval?: number): void; 73 | public static clearPooledTimer(): void; 74 | public static getDatetime(props: MomentProps): any; 75 | } 76 | 77 | export default Moment; 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-moment", 3 | "description": "React component for the moment date library.", 4 | "version": "0.9.7", 5 | "author": { 6 | "name": "Sean Hickey", 7 | "web": "http://headzoo.io" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "^7.4.3", 11 | "@babel/plugin-proposal-class-properties": "^7.4.0", 12 | "@babel/preset-env": "^7.4.3", 13 | "@babel/preset-react": "^7.0.0", 14 | "@babel/preset-stage-2": "^7.0.0", 15 | "@babel/register": "^7.4.0", 16 | "@storybook/addon-actions": "^5.0.6", 17 | "@storybook/addon-knobs": "^5.0.6", 18 | "@storybook/addon-options": "^5.0.6", 19 | "@storybook/addons": "^5.0.6", 20 | "@storybook/react": "^5.0.6", 21 | "babel-eslint": "^10.0.1", 22 | "babel-jest": "^24.7.1", 23 | "babel-loader": "^8.0.5", 24 | "coveralls": "^3.0.3", 25 | "eslint": "^5.16.0", 26 | "eslint-config-airbnb": "^17.1.0", 27 | "eslint-import-resolver-babel-module": "^5.0.1", 28 | "eslint-plugin-import": "^2.16.0", 29 | "eslint-plugin-jsx-a11y": "^6.2.1", 30 | "eslint-plugin-react": "^7.12.4", 31 | "express": "^4.16.4", 32 | "istanbul": "^0.4.5", 33 | "istanbul-api": "^2.1.4", 34 | "istanbul-reports": "^2.2.2", 35 | "jest": "^24.7.1", 36 | "json-loader": "^0.5.4", 37 | "moment": "^2.24.0", 38 | "moment-duration-format": "^2.2.2", 39 | "moment-timezone": "^0.5.23", 40 | "prop-types": "^15.7.2", 41 | "react": "^16.0.0", 42 | "react-dom": "^16.0.0", 43 | "rimraf": "^2.6.3", 44 | "storybook-addon-jsx": "^7.1.0", 45 | "webpack": "^4.29.6", 46 | "webpack-cli": "^3.3.0" 47 | }, 48 | "jest": { 49 | "testRegex": "/tests/.*", 50 | "moduleFileExtensions": [ 51 | "js", 52 | "jsx" 53 | ], 54 | "modulePathIgnorePatterns": [ 55 | "/tests/ssr" 56 | ] 57 | }, 58 | "keywords": [ 59 | "date", 60 | "moment", 61 | "react", 62 | "react-component", 63 | "time" 64 | ], 65 | "license": "MIT", 66 | "main": "dist/index.js", 67 | "peerDependencies": { 68 | "moment": "^2.24.0", 69 | "prop-types": "^15.7.2", 70 | "react": "^15.6.0 || ^16.0.0" 71 | }, 72 | "repository": { 73 | "type": "git", 74 | "url": "git+https://github.com/headzoo/react-moment.git" 75 | }, 76 | "scripts": { 77 | "build": "npm run clean && webpack -p", 78 | "clean": "rimraf ./dist/index.js", 79 | "test": "export TZ=America/New_York; jest --coverage", 80 | "travis-test": "npm run test && cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", 81 | "lint": "eslint ./src --cache --cache-location=.cache/eslint --ext .js,.jsx", 82 | "lint:fix": "npm run lint -- --fix", 83 | "storybook:dev": "start-storybook -p 6006", 84 | "storybook:build": "build-storybook -o stories/build" 85 | }, 86 | "types": "dist/index.d.ts" 87 | } 88 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:react/recommended", "airbnb"], 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true, 7 | "jest": true, 8 | "jasmine": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "max-len": [ 13 | 1, 14 | { 15 | "code": 120, 16 | "ignoreComments": true, 17 | "ignoreUrls": true, 18 | "ignorePattern": "^import\\s.+" 19 | } 20 | ], 21 | "no-unreachable": 0, 22 | "arrow-body-style": 0, 23 | "object-curly-newline": 0, 24 | "no-trailing-spaces": 0, 25 | "react/no-find-dom-node": 0, 26 | "react/no-multi-comp": 0, 27 | "react/forbid-prop-types": 0, 28 | "react/require-default-props": 0, 29 | "react/no-array-index-key": 0, 30 | "jsx-a11y/href-no-hash": 0, 31 | "jsx-quotes": [ 32 | 2, 33 | "prefer-double" 34 | ], 35 | "key-spacing": [ 36 | 2, 37 | { 38 | "singleLine": { 39 | "beforeColon": false, 40 | "afterColon": true 41 | }, 42 | "multiLine": { 43 | "beforeColon": false, 44 | "afterColon": true, 45 | "align": "value" 46 | } 47 | } 48 | ], 49 | "no-multi-spaces": [ 50 | 0, 51 | { 52 | "exceptions": { 53 | "VariableDeclarator": true 54 | } 55 | } 56 | ], 57 | "react/prefer-stateless-function": 0, 58 | "react/jsx-uses-react": 2, 59 | "react/jsx-uses-vars": 2, 60 | "react/react-in-jsx-scope": 2, 61 | "no-var": 2, 62 | "vars-on-top": 0, 63 | "comma-dangle": 0, 64 | "new-cap": 0, 65 | "no-console": 0, 66 | "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }], 67 | "indent": [ 68 | 2, 69 | 2, 70 | { 71 | "SwitchCase": 1 72 | } 73 | ], 74 | "valid-jsdoc": 0, 75 | "import/no-extraneous-dependencies": ["off"], 76 | "jsx-a11y/no-static-element-interactions": 0, 77 | "import/prefer-default-export": "off", 78 | "import/extensions": ["warn", "always", { 79 | "js": "never", "jsx": "never" 80 | }], 81 | "react/no-unused-prop-types": [2, { "skipShapeProps": true }], 82 | "class-methods-use-this": 0, 83 | "jsx-a11y/tabindex-no-positive": 0, 84 | "strict": 0, 85 | "no-param-reassign": 0, 86 | "no-mixed-operators": "off" 87 | }, 88 | "plugins": [ 89 | "react", 90 | "jsx-a11y" 91 | ], 92 | "settings": { 93 | "import/resolver": { 94 | "babel-module": { } 95 | }, 96 | "import/extensions": [".js", ".jsx"], 97 | "react": { 98 | "version": "15.3.2" 99 | // React version, default to the latest React stable release 100 | } 101 | }, 102 | "parserOptions": { 103 | "ecmaFeatures": { 104 | "jsx": true 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import moment from 'moment'; 4 | import 'moment-duration-format'; 5 | import { objectKeyFilter } from './objects'; 6 | 7 | const dateTypes = [ 8 | PropTypes.string, 9 | PropTypes.number, 10 | PropTypes.array, 11 | PropTypes.object 12 | ]; 13 | 14 | const parseTypes = [ 15 | PropTypes.string, 16 | PropTypes.array 17 | ]; 18 | 19 | const calendarTypes = [ 20 | PropTypes.object, 21 | PropTypes.bool 22 | ]; 23 | 24 | export default class Moment extends React.Component { 25 | static propTypes = { 26 | element: PropTypes.any, 27 | date: PropTypes.oneOfType(dateTypes), 28 | parse: PropTypes.oneOfType(parseTypes), 29 | format: PropTypes.string, 30 | add: PropTypes.object, 31 | subtract: PropTypes.object, 32 | ago: PropTypes.bool, 33 | fromNow: PropTypes.bool, 34 | fromNowDuring: PropTypes.number, 35 | from: PropTypes.oneOfType(dateTypes), 36 | toNow: PropTypes.bool, 37 | to: PropTypes.oneOfType(dateTypes), 38 | calendar: PropTypes.oneOfType(calendarTypes), 39 | unix: PropTypes.bool, 40 | utc: PropTypes.bool, 41 | local: PropTypes.bool, 42 | tz: PropTypes.string, 43 | withTitle: PropTypes.bool, 44 | titleFormat: PropTypes.string, 45 | locale: PropTypes.string, 46 | interval: PropTypes.number, 47 | diff: PropTypes.oneOfType(dateTypes), 48 | duration: PropTypes.oneOfType(dateTypes), 49 | durationFromNow: PropTypes.bool, 50 | unit: PropTypes.string, 51 | decimal: PropTypes.bool, 52 | filter: PropTypes.func, 53 | onChange: PropTypes.func 54 | }; 55 | 56 | static defaultProps = { 57 | element: null, 58 | fromNow: false, 59 | toNow: false, 60 | calendar: false, 61 | ago: false, 62 | unix: false, 63 | utc: false, 64 | local: false, 65 | unit: null, 66 | withTitle: false, 67 | decimal: false, 68 | titleFormat: '', 69 | interval: 60000, 70 | filter: (d) => { return d; }, 71 | onChange: () => {} 72 | }; 73 | 74 | static globalMoment = null; 75 | 76 | static globalLocale = null; 77 | 78 | static globalLocal = null; 79 | 80 | static globalFormat = null; 81 | 82 | static globalParse = null; 83 | 84 | static globalFilter = null; 85 | 86 | static globalElement = 'time'; 87 | 88 | static globalTimezone = null; 89 | 90 | static pooledElements = []; 91 | 92 | static pooledTimer = null; 93 | 94 | /** 95 | * Starts the pooled timer 96 | * 97 | * @param {number} interval 98 | */ 99 | static startPooledTimer(interval = 60000) { 100 | Moment.clearPooledTimer(); 101 | Moment.pooledTimer = setInterval(() => { 102 | Moment.pooledElements.forEach((element) => { 103 | if (element.props.interval !== 0) { 104 | element.update(); 105 | } 106 | }); 107 | }, interval); 108 | } 109 | 110 | /** 111 | * Stops the pooled timer 112 | */ 113 | static clearPooledTimer() { 114 | if (Moment.pooledTimer) { 115 | clearInterval(Moment.pooledTimer); 116 | Moment.pooledTimer = null; 117 | Moment.pooledElements = []; 118 | } 119 | } 120 | 121 | /** 122 | * Adds a Moment instance to the pooled elements list 123 | * 124 | * @param {Moment|React.Component} element 125 | */ 126 | static pushPooledElement(element) { 127 | if (!(element instanceof Moment)) { 128 | console.error('Element not an instance of Moment.'); 129 | return; 130 | } 131 | if (Moment.pooledElements.indexOf(element) === -1) { 132 | Moment.pooledElements.push(element); 133 | } 134 | } 135 | 136 | /** 137 | * Removes a Moment instance from the pooled elements list 138 | * 139 | * @param {Moment|React.Component} element 140 | */ 141 | static removePooledElement(element) { 142 | const index = Moment.pooledElements.indexOf(element); 143 | if (index !== -1) { 144 | Moment.pooledElements.splice(index, 1); 145 | } 146 | } 147 | 148 | /** 149 | * Returns a Date based on the set props 150 | * 151 | * @param {*} props 152 | * @returns {*} 153 | */ 154 | static getDatetime(props) { 155 | const { utc, unix } = props; 156 | let { date, locale, parse, tz, local } = props; 157 | 158 | date = date || props.children; 159 | parse = parse || Moment.globalParse; 160 | local = local || Moment.globalLocal; 161 | tz = tz || Moment.globalTimezone; 162 | if (Moment.globalLocale) { 163 | locale = Moment.globalLocale; 164 | } else { 165 | locale = locale || Moment.globalMoment.locale(); 166 | } 167 | 168 | let datetime = null; 169 | if (utc) { 170 | datetime = Moment.globalMoment.utc(date, parse, locale); 171 | } else if (unix) { 172 | // moment#unix fails because of a deprecation, 173 | // but since moment#unix(s) is implemented as moment(s * 1000), 174 | // this works equivalently 175 | datetime = Moment.globalMoment(date * 1000, parse, locale); 176 | } else { 177 | datetime = Moment.globalMoment(date, parse, locale); 178 | } 179 | if (tz) { 180 | datetime = datetime.tz(tz); 181 | } else if (local) { 182 | datetime = datetime.local(); 183 | } 184 | 185 | return datetime; 186 | } 187 | 188 | /** 189 | * Returns computed content from sent props 190 | * @param {*} props 191 | * @returns {*} 192 | * 193 | */ 194 | static getContent(props) { 195 | const { 196 | fromNow, fromNowDuring, from, add, subtract, toNow, to, ago, 197 | calendar, diff, duration, durationFromNow, unit, decimal, 198 | } = props; 199 | 200 | let { format } = props; 201 | 202 | format = format || Moment.globalFormat; 203 | const datetime = Moment.getDatetime(props); 204 | if (add) { 205 | datetime.add(add); 206 | } 207 | if (subtract) { 208 | datetime.subtract(subtract); 209 | } 210 | 211 | const fromNowPeriod = Boolean(fromNowDuring) && -datetime.diff(moment()) < fromNowDuring; 212 | let content = ''; 213 | if (format && !fromNowPeriod) { 214 | content = datetime.format(format); 215 | } else if (from) { 216 | content = datetime.from(from, ago); 217 | } else if (fromNow || fromNowPeriod) { 218 | content = datetime.fromNow(ago); 219 | } else if (to) { 220 | content = datetime.to(to, ago); 221 | } else if (toNow) { 222 | content = datetime.toNow(ago); 223 | } else if (calendar) { 224 | content = datetime.calendar(null, calendar); 225 | } else if (diff) { 226 | content = datetime.diff(diff, unit, decimal); 227 | } else if (duration) { 228 | content = datetime.diff(duration); 229 | } else if (durationFromNow) { 230 | content = moment().diff(datetime); 231 | } else { 232 | content = datetime.toString(); 233 | } 234 | 235 | if (duration || durationFromNow) { 236 | content = moment.duration(content); 237 | content = content.format(format); 238 | } 239 | 240 | const filter = Moment.globalFilter || props.filter; 241 | content = filter(content); 242 | 243 | return content; 244 | } 245 | 246 | /** 247 | * Constructor 248 | * 249 | * @param {*} props 250 | */ 251 | constructor(props) { 252 | super(props); 253 | 254 | if (!Moment.globalMoment) { 255 | Moment.globalMoment = moment; 256 | } 257 | this.state = { 258 | content: '' 259 | }; 260 | this.timer = null; 261 | } 262 | 263 | /** 264 | * Invoked immediately after a component is mounted 265 | */ 266 | componentDidMount() { 267 | this.setTimer(); 268 | if (Moment.pooledTimer) { 269 | Moment.pushPooledElement(this); 270 | } 271 | } 272 | 273 | /** 274 | * Invoked immediately after updating occurs 275 | * 276 | * @param {*} prevProps 277 | */ 278 | componentDidUpdate(prevProps) { 279 | const { interval } = this.props; 280 | 281 | if (prevProps.interval !== interval) { 282 | this.setTimer(); 283 | } 284 | } 285 | 286 | /** 287 | * Invoked immediately before a component is unmounted and destroyed 288 | */ 289 | componentWillUnmount() { 290 | this.clearTimer(); 291 | } 292 | 293 | /** 294 | * Invoked as a mounted component receives new props 295 | * What it returns will become state. 296 | * 297 | * @param {*} nextProps 298 | * @returns {*} (new state) 299 | */ 300 | static getDerivedStateFromProps(nextProps) { 301 | const content = Moment.getContent(nextProps); 302 | return { content }; 303 | } 304 | 305 | /** 306 | * Starts the interval timer. 307 | */ 308 | setTimer = () => { 309 | const { interval } = this.props; 310 | 311 | this.clearTimer(); 312 | if (!Moment.pooledTimer && interval !== 0) { 313 | this.timer = setInterval(() => { 314 | this.update(this.props); 315 | }, interval); 316 | } 317 | }; 318 | 319 | /** 320 | * Returns the element title to use on hover 321 | */ 322 | getTitle = () => { 323 | const { titleFormat } = this.props; 324 | 325 | const datetime = Moment.getDatetime(this.props); 326 | const format = titleFormat || Moment.globalFormat; 327 | 328 | return datetime.format(format); 329 | }; 330 | 331 | 332 | /** 333 | * Clears the interval timer. 334 | */ 335 | clearTimer = () => { 336 | if (!Moment.pooledTimer && this.timer) { 337 | clearInterval(this.timer); 338 | this.timer = null; 339 | } 340 | if (Moment.pooledTimer && !this.timer) { 341 | Moment.removePooledElement(this); 342 | } 343 | }; 344 | 345 | 346 | /** 347 | * Updates this.state.content 348 | * @param {*} props 349 | */ 350 | update(props) { 351 | const elementProps = (props || this.props); 352 | const { onChange } = elementProps; 353 | 354 | const content = Moment.getContent(elementProps); 355 | this.setState({ content }, () => { 356 | onChange(content); 357 | }); 358 | } 359 | 360 | /** 361 | * @returns {*} 362 | */ 363 | render() { 364 | const { withTitle, element, ...remaining } = this.props; 365 | const { content } = this.state; 366 | 367 | const props = objectKeyFilter(remaining, Moment.propTypes); 368 | if (withTitle) { 369 | props.title = this.getTitle(); 370 | } 371 | 372 | return React.createElement( 373 | element || Moment.globalElement, 374 | { 375 | dateTime: Moment.getDatetime(this.props), 376 | ...props 377 | }, 378 | content 379 | ); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /tests/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import TestUtils from 'react-dom/test-utils'; 4 | import moment from 'moment'; 5 | import 'moment-timezone'; 6 | import Moment from '../src/index'; 7 | 8 | const DATE_OUTPUT = 'Mon Apr 19 1976 12:59:00 GMT-0500'; 9 | const DATE_STRING = '1976-04-19T12:59-0500'; 10 | const DATE_DATE = new Date(DATE_STRING); 11 | const DATE_UNIX = DATE_DATE.getTime() / 1000; 12 | 13 | describe('react-moment', () => { 14 | beforeEach(() => { 15 | Moment.globalMoment = null; 16 | Moment.globalLocale = null; 17 | Moment.globalLocal = null; 18 | Moment.globalFormat = null; 19 | Moment.globalParse = null; 20 | Moment.globalFilter = null; 21 | Moment.globalElement = 'time'; 22 | Moment.globalTimezone = null; 23 | Moment.pooledElements = []; 24 | Moment.pooledTimer = null; 25 | }); 26 | 27 | it('children', () => { 28 | let date = TestUtils.renderIntoDocument( 29 | 30 | ); 31 | const expected = moment().toString(); 32 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 33 | 34 | date = TestUtils.renderIntoDocument( 35 | {DATE_STRING} 36 | ); 37 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(DATE_OUTPUT); 38 | 39 | date = TestUtils.renderIntoDocument( 40 | {DATE_DATE} 41 | ); 42 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(DATE_OUTPUT); 43 | 44 | date = TestUtils.renderIntoDocument( 45 | {DATE_UNIX} 46 | ); 47 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(DATE_OUTPUT); 48 | }); 49 | 50 | it('element', () => { 51 | const date = TestUtils.renderIntoDocument( 52 | 1976-04-19 12:59 53 | ); 54 | expect(ReactDOM.findDOMNode(date).tagName).toEqual('SPAN'); 55 | }); 56 | 57 | it('date', () => { 58 | let date = TestUtils.renderIntoDocument( 59 | 60 | ); 61 | const expected = moment().toString(); 62 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 63 | 64 | date = TestUtils.renderIntoDocument( 65 | 66 | ); 67 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(DATE_OUTPUT); 68 | 69 | date = TestUtils.renderIntoDocument( 70 | 71 | ); 72 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(DATE_OUTPUT); 73 | }); 74 | 75 | it('parse', () => { 76 | const date = TestUtils.renderIntoDocument( 77 | 1976-04-19 12:59 78 | ); 79 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(DATE_OUTPUT); 80 | }); 81 | 82 | it('filter', () => { 83 | const filter = (d) => { return d.toUpperCase(); }; 84 | const date = TestUtils.renderIntoDocument( 85 | 1976-04-19 12:59 86 | ); 87 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(DATE_OUTPUT.toUpperCase()); 88 | }); 89 | 90 | it('format', () => { 91 | const date = TestUtils.renderIntoDocument( 92 | {DATE_STRING} 93 | ); 94 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual('1976-04-19'); 95 | }); 96 | 97 | it('add', () => { 98 | const date = TestUtils.renderIntoDocument( 99 | {new Date()} 100 | ); 101 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual('in a day'); 102 | }); 103 | 104 | it('subtract', () => { 105 | const date = TestUtils.renderIntoDocument( 106 | {new Date()} 107 | ); 108 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual('a day ago'); 109 | }); 110 | 111 | it('fromNow', () => { 112 | let date = TestUtils.renderIntoDocument( 113 | {DATE_STRING} 114 | ); 115 | let expected = moment(DATE_STRING).fromNow(); 116 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 117 | 118 | date = TestUtils.renderIntoDocument( 119 | {DATE_STRING} 120 | ); 121 | expected = moment(DATE_STRING).fromNow(true); 122 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 123 | }); 124 | 125 | it('fromNowDuring', () => { 126 | const twoHoursAgo = moment().add(-2, 'hours'); 127 | const hour = 1e3 * 60 * 60; 128 | const TWO_HOURS_AGO_ISO = twoHoursAgo.format(); 129 | 130 | let date = TestUtils.renderIntoDocument( 131 | {TWO_HOURS_AGO_ISO} 132 | ); 133 | let expected = twoHoursAgo.fromNow(); 134 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 135 | 136 | date = TestUtils.renderIntoDocument( 137 | {TWO_HOURS_AGO_ISO} 138 | ); 139 | expected = twoHoursAgo.format('YYYY-MM-DD HH:mm'); 140 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 141 | }); 142 | 143 | it('from', () => { 144 | let date = TestUtils.renderIntoDocument( 145 | {DATE_STRING} 146 | ); 147 | let expected = moment(DATE_STRING).from('2016-09-20T12:00'); 148 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 149 | 150 | date = TestUtils.renderIntoDocument( 151 | {DATE_STRING} 152 | ); 153 | expected = moment(DATE_STRING).from('2016-09-20T12:00', true); 154 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 155 | }); 156 | 157 | it('diff', () => { 158 | let date = TestUtils.renderIntoDocument( 159 | {DATE_STRING} 160 | ); 161 | let expected = moment(DATE_STRING).diff('2016-09-20T12:00').toString(); 162 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 163 | 164 | date = TestUtils.renderIntoDocument( 165 | {DATE_STRING} 166 | ); 167 | expected = moment(DATE_STRING).diff('2016-09-20T12:00', 'days'); 168 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected.toString()); 169 | 170 | date = TestUtils.renderIntoDocument( 171 | 172 | {DATE_STRING} 173 | 174 | ); 175 | expected = moment(DATE_STRING).diff('2016-09-20T12:00', 'years', true); 176 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected.toString()); 177 | 178 | date = TestUtils.renderIntoDocument( 179 | 180 | {DATE_STRING} 181 | 182 | ); 183 | expected = moment(DATE_STRING).diff('2016-09-20T12:00', null, true); 184 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected.toString()); 185 | }); 186 | 187 | it('toNow', () => { 188 | let date = TestUtils.renderIntoDocument( 189 | {DATE_STRING} 190 | ); 191 | let expected = moment(DATE_STRING).toNow(); 192 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 193 | 194 | date = TestUtils.renderIntoDocument( 195 | {DATE_STRING} 196 | ); 197 | expected = moment(DATE_STRING).toNow(true); 198 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 199 | }); 200 | 201 | it('to', () => { 202 | let date = TestUtils.renderIntoDocument( 203 | {DATE_STRING} 204 | ); 205 | let expected = moment(DATE_STRING).to('2016-09-20T12:00'); 206 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 207 | 208 | date = TestUtils.renderIntoDocument( 209 | {DATE_STRING} 210 | ); 211 | expected = moment(DATE_STRING).to('2016-09-20T12:00', true); 212 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 213 | }); 214 | 215 | it('calendar', () => { 216 | let date = TestUtils.renderIntoDocument( 217 | {DATE_STRING} 218 | ); 219 | let expected = moment(DATE_STRING).calendar(); 220 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 221 | 222 | date = TestUtils.renderIntoDocument( 223 | {DATE_STRING} 224 | ); 225 | expected = moment(DATE_STRING).calendar(null, { sameElse: 'YYYY-MM-DD HH:mm' }); 226 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 227 | }); 228 | 229 | it('unix', () => { 230 | const date = TestUtils.renderIntoDocument( 231 | {DATE_UNIX} 232 | ); 233 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(DATE_OUTPUT); 234 | }); 235 | 236 | it('utc', () => { 237 | const date = TestUtils.renderIntoDocument( 238 | 1976-04-19T12:59 239 | ); 240 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual('Mon Apr 19 1976 12:59:00 GMT+0000'); 241 | }); 242 | 243 | it('local', () => { 244 | const date = TestUtils.renderIntoDocument( 245 | {DATE_STRING} 246 | ); 247 | const expected = moment(DATE_STRING).local().toString(); 248 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 249 | }); 250 | 251 | it('utc and local', () => { 252 | const date = TestUtils.renderIntoDocument( 253 | {DATE_STRING} 254 | ); 255 | const expected = moment.utc(DATE_STRING).local().toString(); 256 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 257 | }); 258 | 259 | it('tz', () => { 260 | const date = TestUtils.renderIntoDocument( 261 | {DATE_UNIX} 262 | ); 263 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual('Mon Apr 19 1976 09:59:00 GMT-0800'); 264 | }); 265 | 266 | it('other', () => { 267 | const date = TestUtils.renderIntoDocument( 268 | {DATE_STRING} 269 | ); 270 | expect(ReactDOM.findDOMNode(date).getAttribute('class')).toEqual('testing'); 271 | expect(ReactDOM.findDOMNode(date).getAttribute('aria-hidden')).toEqual('true'); 272 | }); 273 | 274 | it('updates content when props are updated', () => { 275 | const renderInContainer = (component, componentProps = {}) => { 276 | class PropChangeContainer extends React.Component { 277 | constructor(props) { 278 | super(props); 279 | this.state = props; 280 | } 281 | render() { 282 | return React.createElement(component, this.state); 283 | } 284 | } 285 | 286 | const container = TestUtils.renderIntoDocument(); 287 | const instance = TestUtils.findRenderedComponentWithType(container, component); 288 | 289 | return [container, instance]; 290 | }; 291 | 292 | const [container, date] = renderInContainer(Moment, { children: DATE_STRING }); 293 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(DATE_OUTPUT); 294 | 295 | const NEW_DATE_STRING = '1976-04-20T12:59-0500'; 296 | const NEW_DATE_OUTPUT = 'Tue Apr 20 1976 12:59:00 GMT-0500'; 297 | 298 | container.setState({ 299 | children: NEW_DATE_STRING 300 | }); 301 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(NEW_DATE_OUTPUT); 302 | }); 303 | 304 | it('pooled timer', () => { 305 | Moment.startPooledTimer(1); 306 | const date = TestUtils.renderIntoDocument( 307 | 308 | ); 309 | const expected = moment().toString(); 310 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 311 | date.componentWillUnmount(); 312 | }); 313 | 314 | it('globalLocale', () => { 315 | Moment.globalLocale = 'fr'; 316 | const date = TestUtils.renderIntoDocument( 317 | 318 | ); 319 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual('19 avr. 1976'); 320 | }); 321 | 322 | it('globalTimezone', () => { 323 | Moment.globalTimezone = 'America/Los_Angeles'; 324 | const date = TestUtils.renderIntoDocument( 325 | 326 | ); 327 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual('1976-04-19 09'); 328 | }); 329 | 330 | it('globalLocal', () => { 331 | Moment.globalLocal = true; 332 | const date = TestUtils.renderIntoDocument( 333 | {DATE_STRING} 334 | ); 335 | const expected = moment(DATE_STRING).local().toString(); 336 | expect(ReactDOM.findDOMNode(date).innerHTML).toEqual(expected); 337 | }); 338 | 339 | it('globalElement', () => { 340 | Moment.globalElement = 'span'; 341 | const date = TestUtils.renderIntoDocument( 342 | 1976-04-19 12:59 343 | ); 344 | expect(ReactDOM.findDOMNode(date).tagName).toEqual('SPAN'); 345 | }); 346 | 347 | it('globalElement overwrite', () => { 348 | Moment.globalElement = 'span'; 349 | const date = TestUtils.renderIntoDocument( 350 | 1976-04-19 12:59 351 | ); 352 | expect(ReactDOM.findDOMNode(date).tagName).toEqual('DIV'); 353 | }); 354 | }); 355 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-moment 2 | ============ 3 | React component for the [moment](http://momentjs.com/) date library. 4 | 5 | [![Build Status](https://img.shields.io/travis/headzoo/react-moment/master.svg?style=flat-square)](https://travis-ci.org/headzoo/react-moment) 6 | [![Coverage Status](https://img.shields.io/coveralls/github/headzoo/react-moment.svg?style=flat-square)](https://coveralls.io/github/headzoo/react-moment?branch=master) 7 | [![NPM Downloads](https://img.shields.io/npm/dm/react-moment.svg?style=flat-square)](https://www.npmjs.com/package/react-moment) 8 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/headzoo/react-moment/master/LICENSE) 9 | 10 | - [Installing](#installing) 11 | - [Timezone Support](#timezone-support) 12 | - [Quick Start](#quick-start) 13 | - [Props](#props) 14 | - [Interval](#interval) 15 | - [Formatting](#formatting) 16 | - [Parsing Dates](#parsing-dates) 17 | - [Add and Subtract](#add-and-subtract) 18 | - [From Now](#from-now) 19 | - [From Now During](#from-now-during) 20 | - [From](#from) 21 | - [To Now](#to-now) 22 | - [To](#to) 23 | - [Filter](#filter) 24 | - [With Title](#with-title) 25 | - [Title Format](#title-format) 26 | - [Difference](#difference) 27 | - [Duration](#duration) 28 | - [Duration From Now](#duration-from-now) 29 | - [Unix Timestamps](#unix-timestamps) 30 | - [Local](#local) 31 | - [Timezone](#timezone) 32 | - [Calendar](#calendar) 33 | - [Locale](#locale) 34 | - [Element](#element) 35 | - [OnChange](#onchange) 36 | - [Other Props](#other-props) 37 | - [Pooled Timer](#pooled-timer) 38 | - [Global Config](#global-config) 39 | - [Usage with React Native](#usage-with-react-native) 40 | - [License](#license) 41 | - [Contributors](#contributors) 42 | 43 | 44 | ### Installing 45 | Node 6.4.0 or greater is required. Use npm to install `react-moment` along with its peer dependency, `moment`. 46 | 47 | ```sh 48 | npm install --save moment react-moment 49 | ``` 50 | 51 | 52 | ### Timezone Support 53 | The `moment-timezone` package is required to use the timezone related functions. 54 | 55 | ```sh 56 | npm install --save moment-timezone 57 | ``` 58 | 59 | Then import the package into your project. 60 | 61 | ```jsx 62 | import React from 'react'; 63 | import Moment from 'react-moment'; 64 | import 'moment-timezone'; 65 | 66 | export default class App extends React.Component { 67 | ... 68 | } 69 | ``` 70 | 71 | 72 | ### Quick Start 73 | 74 | ```jsx 75 | import React from 'react'; 76 | import Moment from 'react-moment'; 77 | 78 | export default class MyComponent extends React.Component { 79 | render() { 80 | return ( 81 | const dateToFormat = '1976-04-19T12:59-0500'; 82 | {dateToFormat} 83 | ); 84 | } 85 | } 86 | ``` 87 | 88 | Outputs: 89 | 90 | ```html 91 | 92 | ``` 93 | 94 | The above example could also be written this way if you prefer to pass the date using an attribute rather than as a child to ``. 95 | 96 | ```jsx 97 | import React from 'react'; 98 | import Moment from 'react-moment'; 99 | 100 | export default class MyComponent extends React.Component { 101 | render() { 102 | return ( 103 | const dateToFormat = '1976-04-19T12:59-0500'; 104 | 105 | ); 106 | } 107 | } 108 | ``` 109 | 110 | The date value may be a string, object, array, or `Date` instance. 111 | 112 | ```jsx 113 | import React from 'react'; 114 | import Moment from 'react-moment'; 115 | 116 | export default class MyComponent extends React.Component { 117 | render() { 118 | return ( 119 | const dateToFormat = new Date('1976-04-19T12:59-0500'); 120 | 121 | ); 122 | } 123 | } 124 | ``` 125 | 126 | 127 | ### Props 128 | The component supports the following props. See the [Moment docs](https://momentjs.com/docs/) for more information. 129 | 130 | #### Interval 131 | _interval={number}_ 132 | 133 | By default the time updates every 60 seconds (60000 milliseconds). Use the `interval` prop to change or disable updating. 134 | 135 | Updates the time every 30 seconds (30000 milliseconds). 136 | ```jsx 137 | import React from 'react'; 138 | import Moment from 'react-moment'; 139 | 140 | export default class MyComponent extends React.Component { 141 | render() { 142 | return ( 143 | 144 | 1976-04-19T12:59-0500 145 | 146 | ); 147 | } 148 | } 149 | ``` 150 | 151 | Disables updating. 152 | ```jsx 153 | import React from 'react'; 154 | import Moment from 'react-moment'; 155 | 156 | export default class MyComponent extends React.Component { 157 | render() { 158 | return ( 159 | 160 | 1976-04-19T12:59-0500 161 | 162 | ); 163 | } 164 | } 165 | ``` 166 | 167 | 168 | #### Formatting 169 | _format={string}_ 170 | 171 | Formats the date according to the given format string. See the [Moment docs on formatting](https://momentjs.com/docs/#/parsing/string-format/) for more information. 172 | 173 | ```jsx 174 | import React from 'react'; 175 | import Moment from 'react-moment'; 176 | 177 | export default class MyComponent extends React.Component { 178 | render() { 179 | return ( 180 | 181 | 1976-04-19T12:59-0500 182 | 183 | ); 184 | } 185 | } 186 | ``` 187 | 188 | Outputs: 189 | 190 | ```html 191 | 192 | ``` 193 | 194 | 195 | #### Parsing Dates 196 | _parse={string}_ 197 | 198 | Moment can parse most standard date formats. Use the `parse` attribute to tell moment how to parse the given date when non-standard. See the [Moment docs on parsing](https://momentjs.com/docs/#/parsing/string-format/) for more information. 199 | 200 | ```jsx 201 | import React from 'react'; 202 | import Moment from 'react-moment'; 203 | 204 | export default class MyComponent extends React.Component { 205 | render() { 206 | return ( 207 | 208 | 1976-04-19 12:59 209 | 210 | ); 211 | } 212 | } 213 | ``` 214 | 215 | #### Add and Subtract 216 | _add={object}_ 217 | 218 | _subtract={object}_ 219 | 220 | Used to add and subtract periods of time from the given date, with the time periods expressed as object literals. See the [Moment docs on add and subtract](https://momentjs.com/docs/#/manipulating/add/) for more information. 221 | 222 | ```jsx 223 | import React from 'react'; 224 | import Moment from 'react-moment'; 225 | 226 | export default class MyComponent extends React.Component { 227 | render() { 228 | const date = new Date(); 229 | 230 | return ( 231 |
232 | {date} 233 | {date} 234 | {date} 235 | {date} 236 |
237 | ); 238 | } 239 | } 240 | ``` 241 | 242 | #### From Now 243 | _fromNow={bool}_ 244 | 245 | Sometimes called timeago or relative time, displays the date as the time _from now_, e.g. "5 minutes ago". 246 | 247 | ```jsx 248 | import React from 'react'; 249 | import Moment from 'react-moment'; 250 | 251 | export default class MyComponent extends React.Component { 252 | render() { 253 | return ( 254 | 1976-04-19T12:59-0500 255 | ); 256 | } 257 | } 258 | ``` 259 | 260 | Outputs: 261 | 262 | ```html 263 | 264 | ``` 265 | 266 | Including `ago` with `fromNow` will omit the suffix from the relative time. 267 | 268 | ```jsx 269 | import React from 'react'; 270 | import Moment from 'react-moment'; 271 | 272 | export default class MyComponent extends React.Component { 273 | render() { 274 | return ( 275 | 1976-04-19T12:59-0500 276 | ); 277 | } 278 | } 279 | ``` 280 | 281 | Outputs: 282 | 283 | ```html 284 | 285 | ``` 286 | 287 | #### From Now During 288 | 289 | _fromNowDuring={number}_ 290 | 291 | Setting _fromNowDuring_ will display the relative time as with _fromNow_ but just during its value in milliseconds, after that _format_ will be used instead. 292 | 293 | #### From 294 | _from={string}_ 295 | 296 | ```jsx 297 | import React from 'react'; 298 | import Moment from 'react-moment'; 299 | 300 | export default class MyComponent extends React.Component { 301 | render() { 302 | return ( 303 | 1976-04-19T12:59-0500 304 | ); 305 | } 306 | } 307 | ``` 308 | 309 | Outputs: 310 | 311 | ```html 312 | 313 | ``` 314 | 315 | 316 | #### To Now 317 | _toNow={bool}_ 318 | 319 | Similar to `fromNow`, but gives the opposite interval. 320 | 321 | ```jsx 322 | import React from 'react'; 323 | import Moment from 'react-moment'; 324 | 325 | export default class MyComponent extends React.Component { 326 | render() { 327 | return ( 328 | 1976-04-19T12:59-0500 329 | ); 330 | } 331 | } 332 | ``` 333 | 334 | Outputs: 335 | 336 | ```html 337 | 338 | ``` 339 | 340 | 341 | #### To 342 | _to={string}_ 343 | 344 | ```jsx 345 | import React from 'react'; 346 | import Moment from 'react-moment'; 347 | 348 | export default class MyComponent extends React.Component { 349 | render() { 350 | return ( 351 | 1976-04-19T12:59-0500 352 | ); 353 | } 354 | } 355 | ``` 356 | 357 | Outputs: 358 | 359 | ```html 360 | 361 | ``` 362 | 363 | #### Filter 364 | _filter={function}_ 365 | 366 | A function which modifies/transforms the date value prior to rendering. 367 | 368 | ```jsx 369 | import React from 'react'; 370 | import Moment from 'react-moment'; 371 | 372 | export default class MyComponent extends React.Component { 373 | render() { 374 | const toUpperCaseFilter = (d) => { 375 | return d.toUpperCase(); 376 | }; 377 | 378 | return ( 379 | const dateToFormat = '1976-04-19T12:59-0500'; 380 | {dateToFormat} 381 | ); 382 | } 383 | } 384 | ``` 385 | 386 | Outputs: 387 | 388 | ```html 389 | 390 | ``` 391 | 392 | 393 | #### With Title 394 | _withTitle={bool}_ 395 | 396 | Adds a `title` attribute to the element with the complete date. 397 | 398 | ```jsx 399 | import React from 'react'; 400 | import Moment from 'react-moment'; 401 | 402 | export default class MyComponent extends React.Component { 403 | render() { 404 | return ( 405 | 406 | 1976-04-19T12:59-0500 407 | 408 | ); 409 | } 410 | } 411 | ``` 412 | 413 | Outputs: 414 | 415 | ```html 416 | 417 | ``` 418 | 419 | 420 | #### Title Format 421 | _titleFormat={string}_ 422 | 423 | How the `title` date is formatted when using the `withTitle` attribute. 424 | 425 | ```jsx 426 | import React from 'react'; 427 | import Moment from 'react-moment'; 428 | 429 | export default class MyComponent extends React.Component { 430 | render() { 431 | return ( 432 | 433 | 1976-04-19T12:59-0500 434 | 435 | ); 436 | } 437 | } 438 | ``` 439 | 440 | Outputs: 441 | 442 | ```html 443 | 444 | ``` 445 | 446 | 447 | #### Difference 448 | _diff={string}_ 449 | 450 | _decimal={bool}_ 451 | 452 | _unit={string}_ 453 | 454 | ```jsx 455 | import React from 'react'; 456 | import Moment from 'react-moment'; 457 | 458 | export default class MyComponent extends React.Component { 459 | render() { 460 | return ( 461 |
462 | 1976-04-19T12:59-0500 463 | 1976-04-19T12:59-0500 464 | 1976-04-19T12:59-0500 465 |
466 | ); 467 | } 468 | } 469 | ``` 470 | 471 | #### Duration 472 | _duration={string}_ 473 | 474 | _date={string}_ 475 | 476 | Shows the duration (elapsed time) between two dates. `duration` property should be behind `date` property time-wise. 477 | 478 | ```jsx 479 | import React from 'react'; 480 | import Moment from 'react-moment'; 481 | 482 | export default class MyComponent extends React.Component { 483 | render() { 484 | return ( 485 | 488 | ); 489 | } 490 | } 491 | ``` 492 | 493 | #### Duration From Now 494 | 495 | _durationFromNow={bool}_ 496 | 497 | Shows the duration (elapsed time) between now and the provided datetime. 498 | 499 | ```jsx 500 | import React from 'react'; 501 | import Moment from 'react-moment'; 502 | 503 | export default class MyComponent extends React.Component { 504 | render() { 505 | return ( 506 | 509 | ); 510 | } 511 | } 512 | ``` 513 | 514 | #### Unix Timestamps 515 | _unix={bool}_ 516 | 517 | Tells Moment to parse the given date value as a unix timestamp. 518 | 519 | ```jsx 520 | import React from 'react'; 521 | import Moment from 'react-moment'; 522 | 523 | export default class MyComponent extends React.Component { 524 | render() { 525 | const unixTimestamp = 198784740; 526 | return ( 527 | {unixTimestamp} 528 | ); 529 | } 530 | } 531 | ``` 532 | 533 | Outputs: 534 | 535 | ```html 536 | 537 | ``` 538 | 539 | #### Local 540 | _local={bool}_ 541 | 542 | Outputs the result in local time. 543 | 544 | ```jsx 545 | import React from 'react'; 546 | import Moment from 'react-moment'; 547 | 548 | export default class MyComponent extends React.Component { 549 | render() { 550 | return ( 551 | 552 | 2018-11-01T12:59-0500 553 | 554 | ); 555 | } 556 | } 557 | ``` 558 | 559 | Outputs: 560 | 561 | ```html 562 | 563 | ``` 564 | 565 | 566 | #### Timezone 567 | _tz={string}_ 568 | 569 | Sets the timezone. To enable server side rendering (SSR), client and server has to provide same datetime, based on common Timezone. The `tz` attribute will enable set the common timezone. 570 | 571 | ```jsx 572 | import React from 'react'; 573 | import Moment from 'react-moment'; 574 | import 'moment-timezone'; 575 | 576 | export default class MyComponent extends React.Component { 577 | render() { 578 | const unixTimestamp = 198784740; 579 | return ( 580 | 581 | {unixTimestamp} 582 | 583 | ); 584 | } 585 | } 586 | ``` 587 | 588 | Outputs: 589 | 590 | ```html 591 | 592 | ``` 593 | 594 | #### Calendar 595 | _calendar={object|bool}_ 596 | 597 | Customize the strings used for the calendar function. 598 | 599 | ```jsx 600 | import React from 'react'; 601 | import Moment from 'react-moment'; 602 | 603 | export default class MyComponent extends React.Component { 604 | render() { 605 | const calendarStrings = { 606 | lastDay : '[Yesterday at] LT', 607 | sameDay : '[Today at] LT', 608 | nextDay : '[Tomorrow at] LT', 609 | lastWeek : '[last] dddd [at] LT', 610 | nextWeek : 'dddd [at] LT', 611 | sameElse : 'L' 612 | }; 613 | 614 | return ( 615 | 616 | '1976-04-19T12:59-0500' 617 | 618 | ); 619 | } 620 | } 621 | ``` 622 | 623 | 624 | #### Locale 625 | _locale={string}_ 626 | 627 | Sets the locale used to display the date. 628 | 629 | ```jsx 630 | import React from 'react'; 631 | import Moment from 'react-moment'; 632 | 633 | export default class MyComponent extends React.Component { 634 | render() { 635 | const dateToFormat = '1976-04-19T12:59-0500'; 636 | return ( 637 | {dateToFormat} 638 | ); 639 | } 640 | } 641 | ``` 642 | 643 | **Note** 644 | In some cases the language file is not automatically loaded by moment, and it must be manually loaded. For example, to use the French locale, add the following to your bootstrap (e.g. index.js) script. 645 | 646 | ```js 647 | import 'moment/locale/fr'; 648 | ``` 649 | 650 | #### Element 651 | _element={string|React.Component}_ 652 | 653 | The element type to render as (string or function). 654 | 655 | ```jsx 656 | import React from 'react'; 657 | import Moment from 'react-moment'; 658 | 659 | export default class MyComponent extends React.Component { 660 | render() { 661 | return ( 662 | 1976-04-19T12:59-0500 663 | ); 664 | } 665 | } 666 | ``` 667 | 668 | Outputs: 669 | 670 | ```html 671 | Mon Apr 19 1976 12:59:00 GMT-0500 672 | ``` 673 | 674 | 675 | #### OnChange 676 | _onChange={func}_ 677 | 678 | The `onChange` prop is called each time the date is updated, which by default is every 60 seconds. The function receives the new date value. 679 | 680 | ```jsx 681 | import React from 'react'; 682 | import Moment from 'react-moment'; 683 | 684 | export default class MyComponent extends React.Component { 685 | render() { 686 | return ( 687 | { console.log(val); }}> 688 | 1976-04-19T12:59-0500 689 | 690 | ); 691 | } 692 | } 693 | ``` 694 | 695 | 696 | #### Other Props 697 | Any other properties are passed to the `