Giter Club home page Giter Club logo

react-simple-timefield's Introduction

react-simple-timefield

Simple React time input field, check out demo.

Build Status Coverage Status npm npm GitHub license

Demo

Installation

npm install --save react-simple-timefield

#for React <16 use: npm install --save react-simple-timefield@1

Usage

import TimeField from 'react-simple-timefield';
...
<TimeField
    value={time}                       // {String}   required, format '00:00' or '00:00:00'
    onChange={(event, value) => {...}} // {Function} required
    input={<MyCustomInputElement />}   // {Element}  default: <input type="text" />
    inputRef={(ref) => {...}}          // {Function} input's ref
    colon=":"                          // {String}   default: ":"
    showSeconds                        // {Boolean}  default: false
/>

Real world example

import TimeField from 'react-simple-timefield';

class App extends React.Component {
  constructor(...args) {
    super(...args);

    this.state = {
      time: '12:34'
    };

    this.onTimeChange = this.onTimeChange.bind(this);
  }

  onTimeChange(event, time) {
    this.setState({time});
  }

  render() {
    const {time} = this.state;

    return (
      <TimeField value={time} onChange={this.onTimeChange} />
    );
  }
}

Migrate version 2.x to version 3.x

There is a breaking change in version 3. The onChange callback property will be called with two arguments.

// Before:
<TimeField onChange={(value) => console.log(value)} />

// After:
<TimeField onChange={(event, value) => console.log(event, value)} />

Contributing

Run demo:

For running demo locally, replace:

import TimeField from '../';
// to
import TimeField from '../src';

in demo/index.tsx file.

# run development mode
cd demo
npm install
npm install --only=dev
npm run dev

Build:

npm install
npm install --only=dev
npm test
npm run format
npm run build

License

MIT License. Free use and change.

react-simple-timefield's People

Contributors

antonfisher avatar freshollie avatar jamesmontalvo3 avatar nicholaslyang avatar nikolaybaranovv avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

react-simple-timefield's Issues

Error on when trying to install the lib

When I try to add the library I get the following error:

npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR! 
npm ERR! While resolving: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/react
npm ERR!   react@"^18.2.0" from the root project
npm ERR!   peerOptional react@"^16.8.0 || ^17.0.0 || ^18.0.0" from @apollo/[email protected]
npm ERR!   node_modules/@apollo/client
npm ERR!     dev @apollo/client@"^3.7.0" from the root project
npm ERR!     peer @apollo/client@"3.x" from [email protected]
npm ERR!     node_modules/aws-appsync-auth-link
npm ERR!       dev aws-appsync-auth-link@"^3.0.7" from the root project
npm ERR!   27 more (@emotion/react, ...)
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^16.0.0 || ^17" from [email protected]
npm ERR! node_modules/react-simple-timefield
npm ERR!   react-simple-timefield@"^3.2.5" from the root project
npm ERR! 
npm ERR! Conflicting peer dependency: [email protected]
npm ERR! node_modules/react
npm ERR!   peer react@"^16.0.0 || ^17" from [email protected]
npm ERR!   node_modules/react-simple-timefield
npm ERR!     react-simple-timefield@"^3.2.5" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR! 
npm ERR! See /home/circleci/.npm/eresolve-report.txt for a full report.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/circleci/.npm/_logs/2023-01-20T18_34_43_233Z-debug-0.log


Exited with code exit status 1

TypeScript Support

Hi, I'm using your package in a TypeScript app, but I noticed that the package doesn't have any declarations.

I'm open to making a declaration file, but one thought I figured I might float by you is to just convert the code to TypeScript. The package is mostly one file that I can probably convert without too much trouble. That way the declarations will stay up to date even with code changes.

If you have some particular reason or preference for not using TypeScript, then that's perfectly fine too :D

Thanks,
Nicholas

Allow blank dates for validation

I've got a case where the time field is required. Having it default to 00:00 makes it odd because they could miss this field completely and have an undesired 00:00 time.

An allowBlank prop would be useful in this scenario. Visually it could even show --:--.

Add milliseconds

Hi @antonfisher ,

It would also be possible to add milliseconds according to the specifications used in the subtitles field, and use the dot as a placeholder.
Esempio:
00:01:00.000 -> hh:mm:ss:milliseconds -> milliseconds range [000, 999]

Link: codesandbox

import React, { ChangeEvent, CSSProperties, ReactElement } from "react";

const DEFAULT_COLON = ":";
const DEFAULT_COLON_FULL = ".";
const DEFAULT_VALUE_SHORT = `00${DEFAULT_COLON}00`;
const DEFAULT_VALUE_SECOND = `00${DEFAULT_COLON}00${DEFAULT_COLON}00`;
const DEFAULT_VALUE_FULL = `00${DEFAULT_COLON}00${DEFAULT_COLON}00${DEFAULT_COLON_FULL}000`;

export function isNumber<T>(value: T): boolean {
  const number = Number(value);
  return !isNaN(number) && String(value) === String(number);
}

export function formatTimeItem(value?: string | number): string {
  return `${value || ""}00`.substr(0, 2);
}

export function formatTimeFullItem(value?: string | number): string {
  return `${value || ""}000`.substr(0, 3);
}

export function validateTimeAndCursor(
  showSeconds = false,
  showFull = false,
  value = "",
  defaultValue = "",
  colon = DEFAULT_COLON,
  cursorPosition = 0
): [string, number] {
  let newCursorPosition = Number(cursorPosition);

  let [oldH, oldM, oldTempS] = defaultValue.split(colon);
  let [oldS, oldF] = oldTempS.split(DEFAULT_COLON_FULL);

  let [newH, newM, newTempS] = String(value).split(colon);
  let [newS, newF] = newTempS.split(DEFAULT_COLON_FULL);

  //console.log(oldH, oldM, oldS, oldF, oldTempS);
  //console.log(newH, newM, newS, newF, newTempS);

  newH = formatTimeItem(newH);
  if (Number(newH[0]) > 2) {
    newH = oldH;
    newCursorPosition -= 1;
  } else if (Number(newH[0]) === 2) {
    if (Number(oldH[0]) === 2 && Number(newH[1]) > 3) {
      newH = `2${oldH[1]}`;
      newCursorPosition -= 2;
    } else if (Number(newH[1]) > 3) {
      newH = "23";
    }
  }

  newM = formatTimeItem(newM);
  if (Number(newM[0]) > 5) {
    newM = oldM;
    newCursorPosition -= 1;
  }

  if (showFull || showSeconds) {
    newS = formatTimeItem(newS);
    if (Number(newS[0]) > 5) {
      newS = oldS;
      newCursorPosition -= 1;
    }
  }

  if (showFull) newF = formatTimeFullItem(newF);

  const validatedValue = showFull
    ? `${newH}${colon}${newM}${colon}${newS}${DEFAULT_COLON_FULL}${newF}`
    : showSeconds
    ? `${newH}${colon}${newM}${colon}${newS}`
    : `${newH}${colon}${newM}`;

  return [validatedValue, newCursorPosition];
}

type onChangeType = (
  event: ChangeEvent<HTMLInputElement>,
  value: string
) => void;

interface Props {
  value?: string;
  onChange?: onChangeType;
  showSeconds?: boolean;
  showFull?: boolean;
  input: ReactElement | null;
  inputRef?: () => HTMLInputElement | null;
  colon?: string;
  style?: CSSProperties | {};
}

interface State {
  value: string;
  _colon: string;
  _defaultValue: string;
  _showSeconds: boolean;
  _showFull: boolean;
  _maxLength: number;
}

export default class TimeField extends React.Component<Props, State> {
  static defaultProps: Props = {
    showSeconds: false,
    showFull: false,
    input: null,
    style: {},
    colon: DEFAULT_COLON
  };

  constructor(props: Props) {
    super(props);

    const _showSeconds = Boolean(props.showSeconds);
    const _showFull = Boolean(props.showFull);
    const _defaultValue = _showFull
      ? DEFAULT_VALUE_FULL
      : _showSeconds
      ? DEFAULT_VALUE_SECOND
      : DEFAULT_VALUE_SHORT;
    const _colon =
      props.colon && props.colon.length === 1 ? props.colon : DEFAULT_COLON;
    const [validatedTime] = validateTimeAndCursor(
      _showSeconds,
      _showFull,
      this.props.value,
      _defaultValue,
      _colon
    );

    this.state = {
      value: validatedTime,
      _colon,
      _showSeconds,
      _showFull,
      _defaultValue,
      _maxLength: _defaultValue.length
    };

    this.onInputChange = this.onInputChange.bind(this);
  }

  componentDidUpdate(prevProps: Props): void {
    if (this.props.value !== prevProps.value) {
      const [validatedTime] = validateTimeAndCursor(
        this.state._showSeconds,
        this.state._showFull,
        this.props.value,
        this.state._defaultValue,
        this.state._colon
      );
      this.setState({
        value: validatedTime
      });
    }
  }

  onInputChange(
    event: ChangeEvent<HTMLInputElement>,
    callback: onChangeType
  ): void {
    const oldValue = this.state.value;
    const inputEl = event.target;
    const inputValue = inputEl.value;
    const position = inputEl.selectionEnd || 0;
    const isTyped = inputValue.length > oldValue.length;
    const cursorCharacter = inputValue[position - 1];
    const addedCharacter = isTyped ? cursorCharacter : null;
    const removedCharacter = isTyped ? null : oldValue[position];
    const replacedSingleCharacter =
      inputValue.length === oldValue.length ? oldValue[position - 1] : null;
    const colon = this.state._colon;

    let newValue = oldValue;
    let newPosition = position;

    console.log(
      newValue,
      newPosition,
      addedCharacter,
      position,
      this.state._maxLength,
      replacedSingleCharacter
    );

    if (addedCharacter !== null) {
      if (position > this.state._maxLength) {
        newPosition = this.state._maxLength;
      } else if (
        (position === 3 || position === 6) &&
        addedCharacter === colon
      ) {
        newValue = `${inputValue.substr(
          0,
          position - 1
        )}${colon}${inputValue.substr(position + 1)}`;
      } else if (position === 9 && addedCharacter === DEFAULT_COLON_FULL) {
        newValue = `${inputValue.substr(
          0,
          position - 1
        )}${DEFAULT_COLON_FULL}${inputValue.substr(position + 1)}`;
      } else if (
        (position === 3 || position === 6) &&
        isNumber(addedCharacter)
      ) {
        newValue = `${inputValue.substr(
          0,
          position - 1
        )}${colon}${addedCharacter}${inputValue.substr(position + 2)}`;
        newPosition = position + 1;
      } else if (position === 9 && isNumber(addedCharacter)) {
        newValue = `${inputValue.substr(
          0,
          position - 1
        )}${DEFAULT_COLON_FULL}${addedCharacter}${inputValue.substr(
          position + 2
        )}`;
        newPosition = position + 1;
      } else if (isNumber(addedCharacter)) {
        // user typed a number
        newValue =
          inputValue.substr(0, position - 1) +
          addedCharacter +
          inputValue.substr(position + 1);
        if (position === 2 || position === 5 || position === 8) {
          newPosition = position + 1;
        }
      } else {
        // if user typed NOT a number, then keep old value & position
        newPosition = position - 1;
      }
    } else if (replacedSingleCharacter !== null) {
      // user replaced only a single character
      if (isNumber(cursorCharacter)) {
        if (position - 1 === 2 || position - 1 === 5) {
          newValue = `${inputValue.substr(
            0,
            position - 1
          )}${colon}${inputValue.substr(position)}`;
        } else if (position - 1 === 8) {
          newValue = `${inputValue.substr(
            0,
            position - 1
          )}${DEFAULT_COLON_FULL}${inputValue.substr(position)}`;
        } else {
          newValue = inputValue;
        }
      } else {
        // user replaced a number on some non-number character
        newValue = oldValue;
        newPosition = position - 1;
      }
    } else if (
      typeof cursorCharacter !== "undefined" &&
      cursorCharacter !== colon &&
      !isNumber(cursorCharacter)
    ) {
      // set of characters replaced by non-number
      newValue = oldValue;
      newPosition = position - 1;
    } else if (removedCharacter !== null) {
      if ((position === 2 || position === 5) && removedCharacter === colon) {
        newValue = `${inputValue.substr(
          0,
          position - 1
        )}0${colon}${inputValue.substr(position)}`;
        newPosition = position - 1;
      } else if (position === 8 && removedCharacter === DEFAULT_COLON_FULL) {
        newValue = `${inputValue.substr(
          0,
          position - 1
        )}0${DEFAULT_COLON_FULL}${inputValue.substr(position)}`;
        newPosition = position - 1;
      } else {
        // user removed a number
        newValue = `${inputValue.substr(0, position)}0${inputValue.substr(
          position
        )}`;
      }
    }

    const [validatedTime, validatedCursorPosition] = validateTimeAndCursor(
      this.state._showSeconds,
      this.state._showFull,
      newValue,
      oldValue,
      colon,
      newPosition
    );

    this.setState({ value: validatedTime }, () => {
      inputEl.selectionStart = validatedCursorPosition;
      inputEl.selectionEnd = validatedCursorPosition;
      callback(event, validatedTime);
    });

    event.persist();
  }

  render(): ReactElement {
    const { value } = this.state;
    const {
      onChange,
      style,
      showSeconds,
      showFull,
      input,
      inputRef,
      colon,
      ...props
    } = this.props; //eslint-disable-line no-unused-vars
    const onChangeHandler = (event: ChangeEvent<HTMLInputElement>) =>
      this.onInputChange(
        event,
        (e: ChangeEvent<HTMLInputElement>, v: string) =>
          onChange && onChange(e, v)
      );

    if (input) {
      return React.cloneElement(input, {
        ...props,
        value,
        style,
        onChange: onChangeHandler
      });
    }

    return (
      <input
        type="text"
        {...props}
        ref={inputRef}
        value={value}
        onChange={onChangeHandler}
        style={{ width: showFull ? 250 : showSeconds ? 54 : 35, ...style }}
      />
    );
  }
}

Custom colon

Hi @antonfisher, how would you (or would you) recommend customizing the hour/minute separator colon? I'm making a timer and I wanted to toggle the hour/minute colon as a second indicator.

Add 'disabled' prop for input

Thanks for making this awesome library Anton!

I was going to fork this repo and add the feature myself, but it should only take a couple minutes to add a boolean disabled prop and it would be helpful for many users I'm sure! We can't disable the button with pure CSS, and given that this is React we also don't want to circumvent best practices and directly modify the DOM.

Suggestion about input type

Hi,

Thanks a lot for your plugin. Amazing work ! 🥇
But, I noticed that you used "text" for your HTML input type. That's not user friendly for mobile users.
Instead, what about to use "number" type by default ?

Dist contains ES6 modules

As of the latest release, line 36 in the dist/index.js file now contains:

import React from 'react';

This breaks a lot of projects using for example Jest, where items in node_modules are by default not passed through Babel.

People using Jest can add the following to their configs as a temporary workaround, but it's not optimal:

transformIgnorePatterns: ['node_modules/(?!react-simple-timefield)']

Allow to pass empty value

If I set undefined to value prop it throws an error Cannot read property 'split' of undefined. Should be able to have an empty value as a standard <input type="time" /> element.

User is able to type invalid time

Hello.

Thank you for creating this and sharing it with others. I found a small bug I would like to report.
If you select part of the input text and type any character, it will allow it.

Best regards,
Paweł

Show time input in minutes + seconds

Hello.
I was going to use the package on a project for the user to input a time in minutes + seconds and noticed that there's no prop to define that behaviour.
We can show or not seconds but not hours.
I'm going to edit the code and integrate in my project but I think you might want to consider it.

Thank you for your work.

Using ref

im using react-hook-form, i want to use ref and register the input, is there any way to do it ?

Step

It would be great if this allowed for using steps as valid minute input. Eg. 15 = 0, 15, 30, 45

Can't add placeholder

Here expample of my code, where i try to add placeholder, but it's not working.
Maybe you can explain how is possible to add placeholder?

<TimeField
value={time !== '00:00' ? time : undefined}
onChange={onChangeTime}
input={
<InputBase
classes={{ root: classes.inputBase }}
className={classes.inlineInput}
placeholder={"HH:MM"}
onFocus={focus}
/>
}
/>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.