├── .editorconfig ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.json ├── eslint.config.mjs ├── example ├── index.css └── index.tsx ├── index.html ├── jest.config.ts ├── lib ├── DateTimePicker.tsx └── index.ts ├── package.json ├── test └── index.spec.tsx ├── tsconfig.json ├── types └── react-flatpickr.d.ts └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | npm-debug.log 3 | yarn.lock 4 | node_modules/ 5 | coverage/ 6 | build/ 7 | .DS_Store 8 | .idea/ 9 | tsconfig.tsbuildinfo 10 | **/*.js 11 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged && npm test 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "arrowParens": "always", 7 | "bracketSpacing": false, 8 | "endOfLine": "auto", 9 | "htmlWhitespaceSensitivity": "css", 10 | "insertPragma": false, 11 | "jsxSingleQuote": false, 12 | "proseWrap": "preserve", 13 | "quoteProps": "as-needed", 14 | "rangeStart": 0, 15 | "requirePragma": false, 16 | "tabWidth": 2, 17 | "useTabs": false 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 haoxin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version][npm-img]][npm-url] 2 | [![License][license-img]][license-url] 3 | [![Dependency status][david-img]][david-url] 4 | 5 | # react-flatpickr 6 | 7 | [Flatpickr](https://github.com/chmln/flatpickr) for React. 8 | 9 | ## Table of contents 10 | 11 | - [Installation](#installation) 12 | - [Usage](#usage) 13 | - [Basic props](#basic-props) 14 | - [defaultValue](#defaultValue) 15 | - [value](#value) 16 | - [children](#children) 17 | - [className](#className) 18 | - [Event handlers](#event-handlers) 19 | - [Advanced props](#advanced-props) 20 | - [Troubleshooting](#troubleshooting) 21 | 22 | ## Installation 23 | 24 | This package can be install with `yarn` or `npm` 25 | 26 | `npm` 27 | 28 | ```bash 29 | npm install --save react-flatpickr 30 | ``` 31 | 32 | `yarn` 33 | 34 | ```bash 35 | yarn add react-flatpickr 36 | ``` 37 | 38 | ## Usage 39 | 40 | ```jsx 41 | // Keep in mind that these are the styles from flatpickr package 42 | // See troubleshooting section in case you have problems importing the styles 43 | 44 | import "flatpickr/dist/themes/material_green.css"; 45 | 46 | import Flatpickr from "react-flatpickr"; 47 | import { Component } from "react"; 48 | 49 | class App extends Component { 50 | constructor() { 51 | super(); 52 | 53 | this.state = { 54 | date: new Date() 55 | }; 56 | } 57 | 58 | render() { 59 | const { date } = this.state; 60 | return ( 61 | { 65 | this.setState({ date }); 66 | }} 67 | /> 68 | ); 69 | } 70 | } 71 | ``` 72 | 73 | ## Basic props 74 | 75 | ### defaultValue 76 | 77 | > `string` | optional 78 | 79 | This is the default value that will be passed to the inner input 80 | 81 | ### value 82 | 83 | > `string || array || object || number` | optional 84 | 85 | Same as below 86 | 87 | ### options 88 | 89 | > `Object` | optional 90 | 91 | - `Flatpickr options`: you can pass all `Flatpickr parameters` [here](https://flatpickr.js.org/options). 92 | - All `Flatpickr` [hooks][hooks] can be passed within this option too. 93 | 94 | _*Example*_: 95 | 96 | ```jsx 97 | 98 | ``` 99 | 100 | ### children 101 | 102 | > `node` | optional 103 | 104 | This option is closely related with the [wrap option](https://flatpickr.js.org/examples/#flatpickr-external-elements) from `Flatpickr`, please refer to the former link for more information. 105 | 106 | ### className 107 | 108 | > `string` | optional 109 | 110 | Custom className that will be applied to the inner `input` element. In case you need to modify the rendered `input` styles this is the `prop` you should use. 111 | 112 | ## Event handlers 113 | 114 | The following `props` are provided in order to customize the `Flatpickr's functions` default behaviour. Please refer to the [Events & Hooks section](https://flatpickr.js.org/events/) from `Flatpickr` library. 115 | 116 | ### onChange 117 | 118 | > `function` | optional 119 | 120 | ### onOpen: function 121 | 122 | > `function` | optional 123 | 124 | ### onClose: function 125 | 126 | > `function` | optional 127 | 128 | ### onMonthChange: function 129 | 130 | > `function` | optional 131 | 132 | ### onYearChange: function 133 | 134 | > `function` | optional 135 | 136 | ### onReady: function 137 | 138 | > `function` | optional 139 | 140 | ### onValueUpdate: function 141 | 142 | > `function` | optional 143 | 144 | ### onDayCreate: function 145 | 146 | > `function` | optional 147 | 148 | ### onDestroy: function 149 | 150 | > `function` | optional 151 | 152 | ## Advanced props 153 | 154 | ### render prop 155 | 156 | > `function` | optional 157 | 158 | Use this `prop` if you want to `render` your custom component, this is a [Render props pattern](https://reactjs.org/docs/render-props.html). 159 | 160 | _Example usage_: 161 | 162 | ```jsx 163 | import React from 'react'; 164 | import Flatpickr from 'react-flatpickr'; 165 | 166 | const CustomInput = ({ value, defaultValue, inputRef, ...props }) => { 167 | return ; 168 | }; 169 | 170 | export default function App { 171 | return ( 172 | { 175 | return 176 | } 177 | } 178 | /> 179 | ) 180 | } 181 | ``` 182 | 183 | ### flatpickr instance 184 | 185 | You can directly manipulate the [`flatpickr` instance](https://flatpickr.js.org/instance-methods-properties-elements/) using the `flatpickr` property on the component. 186 | 187 | _Example_: 188 | 189 | ```js 190 | import React, { useRef } from "react"; 191 | import Flatpickr from "react-flatpickr"; 192 | 193 | import "flatpickr/dist/flatpickr.css"; 194 | 195 | export default function App() { 196 | const fp = useRef(null); 197 | 198 | return ( 199 |
200 | 201 | 210 |
211 | ); 212 | } 213 | ``` 214 | 215 | ## Themes 216 | 217 | Please import themes directly from the `flatpickr` dependency. 218 | 219 | ## Troubleshooting 220 | 221 | #### Help, the Date Picker doesn't have any styling! 222 | 223 | > In most cases, you should just be able to `import 'flatpickr/dist/themes/airbnb.css'`, but in some cases npm or yarn may install `flatpickr` in `node_modules/react-flatpickr/node_modules/flatpickr`. If that happens, removing your `node_modules` dir and reinstalling should put flatpickr in the root `node_modules` dir, or you can import from `react-flatpickr/node_modules/flatpickr` manually. 224 | 225 | ## License 226 | 227 | MIT 228 | 229 | [npm-img]: https://img.shields.io/npm/v/react-flatpickr.svg?style=flat-square 230 | [npm-url]: https://npmjs.org/package/react-flatpickr 231 | [travis-img]: https://img.shields.io/travis/coderhaoxin/react-flatpickr.svg?style=flat-square 232 | [travis-url]: https://travis-ci.org/coderhaoxin/react-flatpickr 233 | [codecov-img]: https://img.shields.io/codecov/c/github/coderhaoxin/react-flatpickr.svg?style=flat-square 234 | [codecov-url]: https://codecov.io/github/coderhaoxin/react-flatpickr?branch=master 235 | [license-img]: https://img.shields.io/badge/license-MIT-green.svg?style=flat-square 236 | [license-url]: http://opensource.org/licenses/MIT 237 | [david-img]: https://img.shields.io/david/coderhaoxin/react-flatpickr.svg?style=flat-square 238 | [david-url]: https://david-dm.org/coderhaoxin/react-flatpickr 239 | [hooks]: https://chmln.github.io/flatpickr/events/#hooks 240 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", {"targets": {"node": "current"}}], 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import tseslint from 'typescript-eslint'; 4 | import reactHooks from 'eslint-plugin-ts-react-hooks'; 5 | 6 | export default tseslint.config( 7 | {ignores: ['build']}, 8 | { 9 | extends: [js.configs.recommended, ...tseslint.configs.recommended, reactHooks.configs.recommended], 10 | files: ['**/*.{ts,tsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | projectService: { 16 | allowDefaultProject: ['test/index.spec.tsx'] 17 | }, 18 | ecmaFeatures: { 19 | jsx: true 20 | } 21 | } 22 | }, 23 | plugins: {}, 24 | rules: { 25 | '@typescript-eslint/no-explicit-any': 'off' 26 | } 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /example/index.css: -------------------------------------------------------------------------------- 1 | input, 2 | .flatpickr-calendar { 3 | font-family: Arial, Helvetica, sans-serif; 4 | } 5 | html { 6 | font-family: Verdana, Geneva, Tahoma, sans-serif; 7 | } 8 | main { 9 | display: flex; 10 | flex-direction: column; 11 | gap: 1rem; 12 | width: 50vw; 13 | } 14 | div.flatpickr-container { 15 | padding: 0.5rem; 16 | border-radius: 0.25rem; 17 | border: 1px solid gray; 18 | padding: 1rem; 19 | background-color: #eee; 20 | transition: all 200ms ease-in-out; 21 | } 22 | div.flatpickr-container:hover { 23 | border-color: black; 24 | background-color: linen; 25 | } 26 | div.flatpickr-container .title { 27 | font-weight: bold; 28 | margin-bottom: 0.5rem; 29 | } 30 | 31 | div.flatpickr-container button { 32 | margin-left: 0.5rem; 33 | } 34 | div.flatpickr-container dl dt { 35 | font-weight: bold; 36 | } 37 | .custom-class { 38 | padding: 0.5rem; 39 | border-radius: 0.25rem; 40 | border: thin solid black; 41 | background-color: cornsilk; 42 | font-size: 1rem; 43 | font-family: Impact, Haettenschweiler, 'Arial Narrow Bold', sans-serif; 44 | text-align: center; 45 | } 46 | .margin-left { 47 | margin-left: 0.5rem; 48 | } 49 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useRef, useCallback, useMemo} from 'react'; 2 | import {createRoot} from 'react-dom/client'; 3 | import flatpickr from 'flatpickr'; 4 | 5 | import Flatpickr from '../lib/index.js'; 6 | 7 | import 'flatpickr/dist/themes/material_green.css'; 8 | import './index.css'; 9 | 10 | const lastWeek: string = new Date(new Date().setDate(new Date().getDate() - 7)).toISOString().split('T')[0]; 11 | 12 | const App: React.FC = () => { 13 | const calendarRef = useRef(null); 14 | const value = useMemo(() => '2016-01-01 01:01', []); 15 | const onChange = useCallback((_: Date[], str: string) => { 16 | console.info(str); 17 | }, []); 18 | const [range, setRange] = useState([new Date()]); 19 | const [startDate, setStartDate] = useState(new Date()); 20 | const [endDate, setEndDate] = useState(new Date()); 21 | const [handler, setHandler] = useState<(dates: Date[]) => void>(() => (dates: Date[]) => { 22 | console.log('initial handler', dates); 23 | }); 24 | 25 | const sharedOptions = useMemo( 26 | () => ({ 27 | enableTime: true 28 | }), 29 | [] 30 | ); 31 | 32 | const onStartChange = useCallback((date: Date[]) => { 33 | setStartDate(date[0]); 34 | }, []); 35 | 36 | const onEndChange = useCallback((date: Date[]) => { 37 | setEndDate(date[0]); 38 | }, []); 39 | 40 | return ( 41 |
42 |
43 |
44 | Log onChange with time enabled 45 |
46 | { 50 | console.info('First prop handler', str); 51 | }, 52 | (_: Date[], str: string) => { 53 | console.info('Second prop handler', str); 54 | } 55 | ]} 56 | options={{ 57 | ...sharedOptions, 58 | onChange: [ 59 | (_: Date[], str: string) => { 60 | console.info('First options handler', str); 61 | }, 62 | (_: Date[], str: string) => { 63 | console.info('Second options handler', str); 64 | } 65 | ] 66 | }} 67 | /> 68 |
69 |
70 |
71 | Default value,  72 | 73 | time enabled 74 | 75 | , and modify  76 | 77 | onChange 78 | 79 |   handler 80 |
81 | 82 | 92 |
93 |
94 |
Enabled time
95 | console.info(str)} options={sharedOptions} /> 96 |
97 |
98 |
99 | 100 | Set minDate 101 | 102 |   of last week 103 |
104 | console.info(str)} /> 105 |
106 |
107 | 112 | { 116 | setRange(dates); 117 | console.info('range changed', dates, str); 118 | }} 119 | /> 120 |
121 |
122 |
123 | 124 | Set maxDate 125 | 126 |   to today, log in  127 | 128 | prop onOpen 129 | 130 | , log in  131 | 132 | option's onClose 133 | 134 |
135 | { 138 | console.info('opened (by prop)'); 139 | }} 140 | options={{ 141 | onClose: () => { 142 | console.info('closed (by option)'); 143 | }, 144 | maxDate: new Date() 145 | }} 146 | /> 147 |
148 |
149 |
150 | 151 | Preloading the date 152 | 153 |   to today 154 |
155 | console.info(str)} /> 156 |
157 |
158 | 163 | console.info(str)}> 164 | 165 | 168 | 171 | 172 |
173 |
174 | { 177 | calendarRef.current = flatpickr!; 178 | }} 179 | onDestroy={() => { 180 | calendarRef.current = null; 181 | }} 182 | render={({defaultValue}, ref) => { 183 | return ( 184 |
185 | 186 | 187 |
188 | ); 189 | }} 190 | /> 191 |
192 |
193 |
194 | Shared with  195 | 196 | custom class name 197 | 198 |
199 | 200 | 206 | 207 |
208 |
Start
209 |
{startDate?.toString()}
210 |
End
211 |
{endDate?.toString()}
212 |
213 |
214 |
215 | ); 216 | }; 217 | 218 | createRoot(document.querySelector('#container')!).render(); 219 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | example 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | transform: { 4 | '^.+\\.(ts|tsx)$': 'babel-jest', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /lib/DateTimePicker.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef, FC, useMemo, useCallback, useImperativeHandle, ChangeEventHandler} from 'react'; 2 | import flatpickr from 'flatpickr'; 3 | import {Options, DateOption, Plugin, ParsedOptions} from 'flatpickr/dist/types/options'; 4 | import {DateTimePickerProps} from '../types/react-flatpickr'; 5 | import type {OptionsType} from '../types/react-flatpickr'; 6 | 7 | const callbacks = ['onCreate', 'onDestroy'] as const; 8 | const hooks = [ 9 | 'onChange', 10 | 'onOpen', 11 | 'onClose', 12 | 'onMonthChange', 13 | 'onYearChange', 14 | 'onReady', 15 | 'onValueUpdate', 16 | 'onDayCreate' 17 | ] as const; 18 | 19 | const mergeHooks = (inputOptions: flatpickr.Options.Options, props: DateTimePickerProps): OptionsType => { 20 | hooks.forEach((hook: string) => { 21 | const hookFn = props[hook as keyof DateTimePickerProps]; 22 | const existingHookFn = inputOptions[hook as keyof Options]; 23 | if (hookFn) { 24 | if (existingHookFn && !Array.isArray(existingHookFn)) { 25 | (inputOptions as any)[hook] = [(inputOptions as any)[hook]]; 26 | } else if (!(inputOptions as any)[hook]) { 27 | (inputOptions as any)[hook] = []; 28 | } 29 | 30 | const propHook = Array.isArray(hookFn) ? hookFn : [hookFn]; 31 | if ((inputOptions as any)[hook].length === 0) { 32 | (inputOptions as any)[hook] = propHook; 33 | } else { 34 | (inputOptions as any)[hook].push(...propHook); 35 | } 36 | } 37 | }); 38 | 39 | hooks.forEach((hook) => { 40 | delete (props as any)[hook]; 41 | }); 42 | callbacks.forEach((callback) => { 43 | delete (props as any)[callback]; 44 | }); 45 | 46 | return inputOptions; 47 | }; 48 | 49 | export const DateTimePicker: FC = (defaultProps) => { 50 | const props = useMemo(() => ({...defaultProps}), [defaultProps]); 51 | const {defaultValue, options = {}, value, children, render} = props; 52 | const mergedOptions = useMemo(() => mergeHooks(options, props), [options, props]); 53 | const nodeRef = useRef(null); 54 | const flatpickrRef = useRef(undefined); 55 | 56 | useImperativeHandle( 57 | defaultProps.ref, 58 | () => { 59 | return { 60 | get flatpickr() { 61 | return flatpickrRef.current; 62 | } 63 | }; 64 | }, 65 | [] 66 | ); 67 | 68 | useEffect(() => { 69 | const createFlatpickrInstance = () => { 70 | mergedOptions.onClose = 71 | mergedOptions.onClose || 72 | (() => { 73 | if (nodeRef.current?.blur) nodeRef.current.blur(); 74 | }); 75 | 76 | // @ts-expect-error for some reason the default import isnt working correctly 77 | flatpickrRef.current = (flatpickr?.default || flatpickr)(nodeRef.current as HTMLElement, mergedOptions); 78 | 79 | if (flatpickrRef.current && value !== undefined) { 80 | flatpickrRef.current.setDate(value, false); 81 | } 82 | 83 | if (defaultProps.onCreate) defaultProps.onCreate(flatpickrRef.current); 84 | }; 85 | 86 | const destroyFlatpickrInstance = () => { 87 | if (defaultProps.onDestroy) defaultProps.onDestroy(flatpickrRef.current); 88 | if (flatpickrRef.current) { 89 | flatpickrRef.current.destroy(); 90 | } 91 | flatpickrRef.current = undefined; 92 | }; 93 | 94 | createFlatpickrInstance(); 95 | 96 | if (flatpickrRef.current) { 97 | const optionsKeys = Object.getOwnPropertyNames(mergedOptions); 98 | for (let index = optionsKeys.length - 1; index >= 0; index--) { 99 | const key = optionsKeys[index]; 100 | let optionValue = mergedOptions[key as keyof OptionsType]; 101 | 102 | if (optionValue?.toString() !== flatpickrRef.current.config[key as keyof ParsedOptions]?.toString()) { 103 | if (hooks.includes(key as any) && !Array.isArray(optionValue)) { 104 | optionValue = [optionValue] as unknown as Plugin; 105 | } 106 | 107 | flatpickrRef.current.set(key as any, optionValue); 108 | } 109 | } 110 | 111 | if (value !== undefined && value !== flatpickrRef.current.input.value) { 112 | flatpickrRef.current.setDate(value as DateOption | DateOption[], false); 113 | } 114 | } 115 | 116 | return () => { 117 | destroyFlatpickrInstance(); 118 | }; 119 | }, [mergedOptions, options, props, value, defaultProps]); 120 | 121 | const handleNodeChange = useCallback((node: HTMLElement | null) => { 122 | nodeRef.current = node; 123 | }, []); 124 | 125 | if (render) { 126 | return render({...props, defaultValue, value}, handleNodeChange); 127 | } 128 | 129 | const onChange: ChangeEventHandler = useCallback( 130 | (e) => { 131 | if (defaultProps && defaultProps.onChange) { 132 | if (Array.isArray(defaultProps?.onChange)) { 133 | defaultProps?.onChange?.forEach(() => [new Date(e.target.value)], value?.toString() || ''); 134 | } else if (typeof defaultProps.onChange === 'function') { 135 | defaultProps?.onChange?.([new Date(e.target.value)], value?.toString() || '', flatpickrRef.current!); 136 | } 137 | } 138 | }, 139 | [defaultProps, value] 140 | ); 141 | 142 | return options.wrap ? ( 143 |
144 | {children} 145 |
146 | ) : ( 147 | 155 | ); 156 | }; 157 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import {DateTimePicker} from './DateTimePicker'; 2 | // @ts-expect-error not sure why I have to force the types to be exported 3 | export * from '../types/react-flatpickr.d.ts'; 4 | export default DateTimePicker; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-flatpickr", 3 | "version": "4.0.10", 4 | "description": "flatpickr for React", 5 | "exports": { 6 | ".": { 7 | "import": { 8 | "types": "./build/react-flatpickr.d.ts", 9 | "default": "./build/react-flatpickr.js" 10 | }, 11 | "require": { 12 | "types": "./build/react-flatpickr.d.ts", 13 | "default": "./build/react-flatpickr.cjs" 14 | } 15 | } 16 | }, 17 | "type": "module", 18 | "types": "./build/react-flatpickr.d.ts", 19 | "main": "./build/react-flatpickr.cjs", 20 | "module": "./build/react-flatpickr.js", 21 | "scripts": { 22 | "lint": "eslint --quiet --fix lib test types", 23 | "format": "prettier --config .prettierrc --write 'lib/*.{ts,tsx}' 'example/*' 'test/*.{ts,tsx}' 'types/*.ts'", 24 | "build": "tsc -b && vite build", 25 | "test": "jest --config=jest.config.ts --env=jsdom", 26 | "test:watch": "jest --config=jest.config.ts --watch --env=jsdom", 27 | "test:coverage": "jest --config=jest.config.ts --coverage --env=jsdom", 28 | "prepublishOnly": "npm run build", 29 | "examples": "vite --port 3000 --open", 30 | "prepare": "husky" 31 | }, 32 | "lint-staged": { 33 | "*.{ts,tsx,js,css}": [ 34 | "eslint --fix", 35 | "prettier --config .prettierrc --write" 36 | ] 37 | }, 38 | "repository": "haoxins/react-flatpickr", 39 | "keywords": [ 40 | "flatpickr", 41 | "react" 42 | ], 43 | "files": [ 44 | "build" 45 | ], 46 | "author": "haoxin", 47 | "license": "MIT", 48 | "dependencies": { 49 | "flatpickr": "^4.6.13" 50 | }, 51 | "devDependencies": { 52 | "@babel/cli": "^7.26.4", 53 | "@babel/core": "^7.26.10", 54 | "@babel/eslint-parser": "^7.26.5", 55 | "@babel/plugin-proposal-decorators": "^7.25.9", 56 | "@babel/plugin-proposal-do-expressions": "^7.25.9", 57 | "@babel/plugin-proposal-export-default-from": "^7.25.9", 58 | "@babel/plugin-proposal-function-bind": "^7.25.9", 59 | "@babel/plugin-proposal-function-sent": "^7.25.9", 60 | "@babel/plugin-proposal-pipeline-operator": "^7.26.7", 61 | "@babel/plugin-proposal-throw-expressions": "^7.25.9", 62 | "@babel/preset-env": "^7.26.9", 63 | "@babel/preset-react": "^7.26.3", 64 | "@babel/preset-typescript": "^7.27.0", 65 | "@eslint/js": "^9.19.0", 66 | "@jest/globals": "^29.7.0", 67 | "@testing-library/jest-dom": "^6.6.3", 68 | "@testing-library/react": "^16.2.0", 69 | "@types/react": "^19.1.0", 70 | "@vitejs/plugin-react": "^4.3.4", 71 | "babel-jest": "^29.7.0", 72 | "css-loader": "^7.1.2", 73 | "eslint": "^9.23.0", 74 | "eslint-plugin-react": "^7.37.4", 75 | "eslint-plugin-ts-react-hooks": "^1.0.4", 76 | "globals": "^16.0.0", 77 | "husky": "^9.1.7", 78 | "jest": "^29.7.0", 79 | "jest-environment-jsdom": "^29.7.0", 80 | "lint-staged": "^15.5.1", 81 | "prettier": "^3.5.3", 82 | "react": "^19.1.0", 83 | "react-dom": "^19.1.0", 84 | "ts-node": "^10.9.2", 85 | "typescript-eslint": "^8.29.1", 86 | "vite": "^6.2.3", 87 | "vite-plugin-dts": "^4.5.3" 88 | }, 89 | "peerDependencies": { 90 | "react": ">= 16 <= 19" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import React, {Ref} from 'react'; 2 | import {jest, expect, describe, it} from '@jest/globals'; 3 | import {fireEvent, render} from '@testing-library/react'; 4 | import DateTimePicker from '../lib'; 5 | import {Instance} from 'flatpickr/dist/types/instance'; 6 | 7 | describe('react-flatpickr', () => { 8 | it('shows an empty input', () => { 9 | const {unmount, container} = render(); 10 | const input = container.querySelector('input'); 11 | expect(input?.value).toBe(''); 12 | unmount(); 13 | }); 14 | 15 | describe('#value', () => { 16 | describe('is in the YYYY-MM-DD format', () => { 17 | it('shows it in the input', () => { 18 | const {unmount, container} = render(); 19 | const input = container.querySelector('input'); 20 | expect(input?.value).toBe('2000-01-01'); 21 | unmount(); 22 | }); 23 | }); 24 | 25 | describe('is in the YYYY.MM.DD format', () => { 26 | it('normalizes it and shows in the input', () => { 27 | const {unmount, container} = render(); 28 | const input = container.querySelector('input'); 29 | expect(input?.value).toBe('2000-01-01'); 30 | unmount(); 31 | }); 32 | }); 33 | 34 | describe('is updated with a minDate', () => { 35 | it('updates the minDate first', () => { 36 | const {unmount, rerender, container} = render( 37 | 38 | ); 39 | const input = container.querySelector('input'); 40 | expect(input?.value).toBe('2000-01-01'); 41 | rerender(); 42 | expect(input?.value).toBe('1999-01-01'); 43 | unmount(); 44 | }); 45 | }); 46 | }); 47 | 48 | describe('#render', () => { 49 | it('is possible to provide a custom input', () => { 50 | function MaskedInput({defaultValue, innerRef}: {defaultValue: any; innerRef: Ref | undefined}) { 51 | return ; 52 | } 53 | 54 | const {unmount, container} = render( 55 | { 58 | return ( 59 |
60 | 61 | bar 62 |
63 | ); 64 | }} 65 | /> 66 | ); 67 | const input = container.querySelector('input'); 68 | const span = container.querySelector('span'); 69 | expect(input?.value).toEqual('2000-01-01'); 70 | expect(span).toBeDefined(); 71 | unmount(); 72 | }); 73 | }); 74 | 75 | describe('#onCreate', () => { 76 | it('is called when the flatpickr instance is created', () => { 77 | const spy = jest.fn(); 78 | const {unmount} = render(); 79 | expect(spy).toHaveBeenCalled(); 80 | unmount(); 81 | }); 82 | 83 | it('is possible to reference the flatpickr instance', () => { 84 | let calendar: Instance | null | undefined; 85 | const {unmount, container} = render( 86 | { 89 | calendar = flatpickr; 90 | }} 91 | render={({defaultValue}, ref) => { 92 | return ( 93 |
94 | 95 | 102 |
103 | ); 104 | }} 105 | /> 106 | ); 107 | const input = container.querySelector('input'); 108 | expect(input?.value).toEqual('2000-01-01'); 109 | const button = container.querySelector('button') as HTMLButtonElement; 110 | fireEvent.click(button); 111 | expect(input?.value).toEqual('1000-01-01'); 112 | unmount(); 113 | }); 114 | }); 115 | 116 | describe('#onDestroy', () => { 117 | it('is called when the flatpickr instance is destroyed', () => { 118 | const spy = jest.fn(); 119 | const {unmount} = render(); 120 | unmount(); 121 | expect(spy).toHaveBeenCalled(); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "CommonJS", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": false, 9 | "jsx": "react-jsx", 10 | "declaration": true, 11 | "outDir": "build", 12 | "types": ["vite/client", "node", "react"] 13 | }, 14 | "include": ["lib/**/*"], 15 | "exclude": ["node_modules", "*.json", "example/**/*", "types/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /types/react-flatpickr.d.ts: -------------------------------------------------------------------------------- 1 | import {ComponentPropsWithoutRef, ReactNode, Ref} from 'react'; 2 | import flatpickr from 'flatpickr'; 3 | import {DateOption, Options} from 'flatpickr/dist/types/options'; 4 | 5 | type Omit = Pick>; 6 | 7 | export type Callback = (arg0?: flatpickr.Instance | null) => void; 8 | 9 | export type OptionsType = { 10 | [k in keyof Options]?: Options[k]; 11 | }; 12 | 13 | export interface DateTimePickerHandle { 14 | flatpickr?: flatpickr.Instance; 15 | } 16 | 17 | export interface DateTimePickerProps 18 | extends Omit, 'children' | 'value' | 'onChange'> { 19 | defaultValue?: string; 20 | options?: OptionsType; 21 | onChange?: flatpickr.Options.Hook[] | flatpickr.Options.Hook; 22 | onOpen?: flatpickr.Options.Hook[] | flatpickr.Options.Hook; 23 | onClose?: flatpickr.Options.Hook[] | flatpickr.Options.Hook; 24 | onMonthChange?: flatpickr.Options.Hook[] | flatpickr.Options.Hook; 25 | onYearChange?: flatpickr.Options.Hook[] | flatpickr.Options.Hook; 26 | onReady?: flatpickr.Options.Hook[] | flatpickr.Options.Hook; 27 | onValueUpdate?: flatpickr.Options.Hook[] | flatpickr.Options.Hook; 28 | onDayCreate?: flatpickr.Options.Hook[] | flatpickr.Options.Hook; 29 | onCreate?: Callback; 30 | onDestroy?: Callback; 31 | 32 | value?: DateOption | DateOption[]; 33 | children?: ReactNode; 34 | className?: string; 35 | ref?: Ref; 36 | 37 | render?: (props: any, handleNodeChange: (node: HTMLElement | null) => void) => ReactNode; 38 | } 39 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | import react from '@vitejs/plugin-react'; 3 | import dts from 'vite-plugin-dts'; 4 | import {defineConfig} from 'vite'; 5 | 6 | const packageJSON = require('./package.json'); 7 | 8 | export default defineConfig({ 9 | plugins: [dts({rollupTypes: true}), react()], 10 | build: { 11 | sourcemap: true, 12 | lib: { 13 | entry: resolve(__dirname, 'lib/index.ts'), 14 | name: packageJSON.name, 15 | formats: ['cjs', 'es'], 16 | }, 17 | outDir: 'build', 18 | rollupOptions: { 19 | external: [ 20 | 'react', 21 | 'react-dom', 22 | 'react/jsx-runtime', 23 | ...Object.keys(packageJSON.dependencies), 24 | ...Object.keys(packageJSON.peerDependencies), 25 | ], 26 | output: { 27 | globals: { 28 | react: 'React', 29 | 'react-dom': 'ReactDOM' 30 | } 31 | } 32 | } 33 | } 34 | }); 35 | --------------------------------------------------------------------------------