类型“ ReactInstance”上不存在属性“ selectionStart”。属性'selectionStart'在'Component <any,{},any>'类型上不存在。

时间:2020-10-26 02:59:54

标签: reactjs typescript

我收到打字稿错误。在这里,我正在共享代码,但出现错误

代码:1 DateField.tsx

handleDate(value: string){ 
    const inputField  = this.refs.input;
    const caretStart = inputField.selectionStart ;
    const caretEnd = inputField.selectionEnd;

错误:1

任何 类型“ ReactInstance”上不存在属性“ selectionStart”。 属性'selectionStart'在类型'Component '上不存在。ts(2339)

代码:2 DateField.tsx

completeField(value:any, fieldIndex:any) {
    return _.padStart(
      value,
      FIELDS[fieldIndex].label.length,
      FIELDS[fieldIndex].pad
    );
  }

错误:2

(财产)垫子:号码 无法将类型'number'的参数分配给类型'string |的参数。 undefined'.ts(2345)

代码:3 DropDown.tsx

componentDidUpdate() {
    if (this.refs.list) {
      const option = this.refs["option-" + this.props.value];
      this.refs.list._scrollTop =
        option.offsetTop - (this.props.height - option.offsetHeight) / 2;
    }
  }

错误:3
任何 类型“ ReactInstance”上不存在属性“ _scrollTop”。 属性'_scrollTop'在'Component '类型上不存在.ts(

代码:4 DropDown.tsx

render() {
    const { value, isOpen } = this.state;
    const current = _.find(this.props.options, (option) => {
      // HACK because select returns string
      return (
        option.value === value || option.value.toString() === value.toString()
      );
    });

错误:4
const值:字符串 由于类型'number'和'string'没有重叠,因此此条件将始终返回'false'。ts(2367)

DateField.tsx

import _ from "lodash";
import moment from "moment";
import React, { Component } from "react";
import "./DateField.scss";

const SEPARATOR = "-";
const POSSIBLE_SEPARATORS = ["-", "/", " ", String.fromCharCode(13)];
const FIELDS = [
  {
    label: "DD",
    pad: 0
  },
  {
    label: "MM",
    pad: 0
  },
  {
    label: "YYYY",
    pad: moment().year()
  }
];

const HINT = FIELDS.map((field) => field.label).join(SEPARATOR);
const MAX_LENGTH = HINT.length;

type OnChangePropType = {
  day: number;
  month: number;
  year: number;
  value: any;
  resolvedDate: any;
};


interface IProps {
  value: any;
  onChange: (param: OnChangePropType) => void;
}

interface IState {
  value: any;
  errors: Array<Object>;
  hint: string;
}

export class DateField extends Component<IProps, IState> {
  static defaultProps = {
    onChange: () => {}
  };

  constructor(props: IProps) {
    super(props);
    this.state = {
      value: props.value || "",
      hint: HINT,
      errors: []
    };
  }

  /**
   * Find difference between words (i.e. insertion or edit point)
   * @param {*} value
   * @param {*} lastValue
   */
  findDifference(value:any, lastValue:any) {
    for (let i = 0; i < value.length && i < lastValue.length; i++) {
      if (value[i] !== lastValue[i]) {
        return i;
      }
    }
    return value.length - 1;
  }

  findClosestSeparatorFieldIndex({ value, editIndex }:{ value:any, editIndex:any }) {
    const partialValue = value.substr(0, editIndex + 1);
    let numSeparators = partialValue.match(new RegExp(SEPARATOR, "g"));

    if (numSeparators) {
      // FIELD index from zero (['DD', 'MM', 'YYYY'])
      return numSeparators.length - 1;
    }
    return null;
  }

  componentDidUpdate(prevProp:IProps) {
    const { value } = this.props;
    if(prevProp.value !==  value && value) {
      this.handleDate(value);
    }
  }

  handleDate(value: string){ 
    const inputField  = this.refs.input;
    const caretStart = inputField.selectionStart ;
    const caretEnd = inputField.selectionEnd;

    // e.preventDefault();
    let { hint } = this.state;
    // let value = val;
    console.log("value", value);
    const errors = [];

    // swap all possible separators for correct one
    value = value.replace(
      new RegExp(`[${POSSIBLE_SEPARATORS.join("")}]`, "g"),
      SEPARATOR
    );

    // remove non-valid chars (not sep or digit)
    value = value.replace(new RegExp(`[^${SEPARATOR}0-9]`, ""), "");

    let editIndex = this.findDifference(value, this.state.value);
    let fieldToCompleteIndex = null;

    // find attempts at splitting
    if (value.charAt(editIndex) === SEPARATOR) {
      // const allSeparators = new RegExp(SEPARATOR, 'g').exec(value);
      // console.log('all', allSeparators);
      // const closestSeparator = _.find(allSeparators, (match, i) => {
      //   return editIndex < match.index ? i : false;
      // });

      fieldToCompleteIndex = this.findClosestSeparatorFieldIndex({
        value,
        editIndex
      });
      // console.log(fieldToCompleteIndex);
      // if (editIndex >
      // if (editIndex < 2) {
      //   completeComponent = 'day';
      // }
      // console.log(editIndex, 'YES');
    }

    // fix value by removing non-digits
    value = value.replace(/[^0-9]/g, "");
    const maxLength = HINT.replace(SEPARATOR, "").length;

    // size limit
    if (value.length > maxLength) {
      value = value.substr(0, maxLength);
    }

    // split into fields
    let day = value.substr(0, 2);
    let month = value.substr(2, 2);
    let year = value.substr(4, 4);

    // const resolvedDate = this.resolveDate({ day, month, year })
    // console.log(resolvedDate);

    if (fieldToCompleteIndex === 0) {
      day = this.completeField(day, fieldToCompleteIndex);
    }
    if (fieldToCompleteIndex === 1) {
      month = this.completeField(month, fieldToCompleteIndex);
    }
    if (fieldToCompleteIndex === 2) {
      year = this.completeField(year, fieldToCompleteIndex);
    }
    // editIndex++;

    let resolvedDate = null;
    if (day && month && year) {
      resolvedDate = moment([year, +month -1  , day]);
      if (!resolvedDate.isValid()) {
        errors.push("Invalid");
        // console.log(resolvedDate);
      }
    }

    value =
      day +
      (month || fieldToCompleteIndex === 0 ? SEPARATOR + month : "") +
      (year || fieldToCompleteIndex === 1 ? SEPARATOR + year : "");

    // edit hint to remove replaced chars
    hint = HINT.substr(value.length);

    this.setState({ value, hint, errors });
    this.props.onChange({
      day: parseInt(day, 10),
      month: parseInt(month, 10) - 1,
      year: parseInt(year, 10),
      value,
      resolvedDate
    });
    // console.log(
    //   "caretStart",
    //   caretStart,
    //   "caretEnd",
    //   caretEnd,
    //   "editIndex",
    //   editIndex
    // );
  }

  change(e: React.ChangeEvent<HTMLInputElement>) {
   this.handleDate(e.target.value.toString());
    // requestAnimationFrame(() => {
    //   inputField.selectionStart = editIndex;
    //   inputField.selectionEnd = editIndex;
    // });
  }

  // resolveDate({ day, month, year }) {
  //   const today = moment();
  //   day  = parseInt(day) || 1;
  //   month = parseInt(month) || 0;
  //   year = parseInt(year) || today.year();

  //   let resolvedDate = moment([year, month, day]);
  //   console.log(resolvedDate);
  //   // if (parseInt(day) > ) {
  //   //   day = 1;
  //   // }
  //   if (!month || parseInt(month) === 0) {
  //     month = today.month();
  //   }

  //   return {
  //     day,
  //     month,
  //     year,
  //   };
  // }
  
/**
   * Find difference between words (i.e. insertion or edit point)
   * @param {*} length
   * @param {*} lastValue
   */
  completeField(value:any, fieldIndex:any) {
    return _.padStart(
      value,
      FIELDS[fieldIndex].label.length,
      FIELDS[fieldIndex].pad
    );
  }

  render() {
    const { value, hint, errors } = this.state;
    return (
      <div>
        <div className="field">
          <div className="field-hint">
            <span className="hint-filled">{value}</span>
            {hint}
          </div>
          <input
            className="field-input"
            onChange={(e) => this.change(e)}
            value={value}
            ref="input"
          />
        </div>
        <div className="field-errors">
          {errors.map((error: React.ReactNode, i: number) => (
            <div className="field-errors-item" key={i}>
              {error}
            </div>
          ))}
        </div>
      </div>
    );
  }
}

DropDOwn.tsx

import _ from "lodash";
import React, { Component } from "react";
import DropdownArrow from "../../assets/dropdown-arrow.svg";
import "./Dropdown.scss";

type OptionsPropType = {
  value: number;
  label: string;
};

interface IProps {
  value: string;
  height: number;
  options: Array<OptionsPropType>;
  onChange: (value: string) => void;
}

interface IState {
  isOpen: boolean;
  value: string;
}

export class Dropdown extends Component<IProps, IState> {
  listRef = null;

  constructor(props:IProps) {
    super(props);
    this.state = {
      isOpen: false,
      value: props.value
    };
  }

  change(value:string) {
    this.setState({ value });
    this.props.onChange(value);
    this.close();
  }

  onOutsideClicked = (e:any) => {
    // only outside clicks allowed
    // if (this.refs.container && this.refs.container.contains(e.target)) {
    //   return;
    // }
    // document.removeEventListener("mousedown", this.onOutsideClicked);
    this.close();
  };

  toggle() {
    const isOpen = !this.state.isOpen;
    this.setState({ isOpen });
    if (isOpen) {
      document.addEventListener("mousedown", this.onOutsideClicked);
    }
  }

  close() {
    this.setState({ isOpen: false });
  }

  componentWillReceiveProps(props) {
    if (props.value !== this.state.value) {
      this.setState({ value: props.value });
    }
  }

  componentDidUpdate() {
    if (this.refs.list) {
      const option = this.refs["option-" + this.props.value];
      this.refs.list._scrollTop =
        option.offsetTop - (this.props.height - option.offsetHeight) / 2;
    }
  }

  render() {
    const { value, isOpen } = this.state;
    const current = _.find(this.props.options, (option) => {
      // HACK because select returns string
      return (
        option.value === value || option.value.toString() === value.toString()
      );
    });

    const list = _.map(this.props.options, (option) => (
      <div
        ref={`option-${option.value}`}
        className={`dropdown-option ${
          value === option.value ? "dropdown-option--selected" : ""
        }`}
        onClick={() => this.change(option.value)}
        key={option.value}
      >
        {option.label}
      </div>
    ));

    return (
      <div
        className={`dropdown ${isOpen ? "dropdown--open" : ""}`}
        onClick={() => this.toggle()}
        ref="container"
      >
        <div className="dropdown-label">{current.label}</div>
        <div className="dropdown-arrow">
          <img src={DropdownArrow} />
        </div>
        {/* <select onChange={(e) => this.change(e.target.value)} defaultValue={value}>{list}</select> */}
        {isOpen ? (
          <div
            className="dropdown-list"
            style={{ maxHeight: this.props.height + "px" }}
            ref="list"
          >
            {list}
          </div>
        ) : null}
      </div>
    );
  }
}

Widget.tsx

import React, { Component } from "react";
import ReactDOM from "react-dom";
import moment from "moment";
import { find as _find } from "lodash";
import { Dropdown } from "./dropdown/Dropdown";
import { DateField } from "./date-field/DateField";
import { MonthNavigator } from "./month-navigator/MonthNavigator";
import "./Widget.scss";

const MIN_YEAR = 1900;
const MAX_YEAR = moment().year() + 5;

type DatePropType = {
  day: number;
  month: number;
  year: number;
};

type FromPropType = {
  day: number;
  month: number;
  year: number;
};

type UntilPropType = {
  day: number;
  month: number;
  year: number;
};

type FromToUntilPropType = {
  from: FromPropType;
  until: UntilPropType;
};

interface IProps {
  disabledRanges: Array<FromToUntilPropType>;
  date:Date
}

interface IState {
  date: Date;
  day: number;
  month: number;
  year: number;
  selectedDay: number;

  selectedMonth: number;
  selectedYear: number;
  dropdownHeight: number;
  selectedDate: string;
}

export class Widget extends Component<IProps, IState> {
  constructor(props: IProps) {
    super(props);
    const date = props.date || moment();
    this.state = {
      date,
      day: date.date(),
      month: date.month(),
      year: date.year(),
      selectedDay: 10,
      selectedMonth: 2,
      selectedYear: 2019,
      dropdownHeight: 200,
      selectedDate: '',
    };
  }

  // createDay({ dayNumber, startDay }) {
  //   const { day, month, year, date, selectedDay } = this.state;

  //   let state = '';
  //   if (month === date.month() && year === date.year()) {
  //     state = dayNumber === selectedDay
  //       ? 'Widget-day--selected'
  //       : (dayNumber === day
  //         ? 'Widget-day--current'
  //         : ''
  //       );
  //   }
  //   const style = {};
  //   if (startDay) {
  //     style = { ...style, gridColumnStart: startDay};
  //     startDay = 0;
  //   }
  //   return (
  //     <div className={`Widget-day ${state}`} onClick={() => this.changeDay({ day: dayNumber)}
  //       style={style}
  //       key={dayNumber}>
  //       {dayNumber}
  //     </div>
  //   );
  // }

  /**
   *
   * @param {object} param
   * @param {string} param.type   Can be: 'previous' | 'next' | `null` where `null` is current
   */
  createDays() {
    const daysInWeek = 7;
    const {
      day,
      month,
      year,
      date,
      selectedDay,
      selectedMonth,
      selectedYear
    } = this.state;
    const list = [];
    const daysInCurrentMonth = moment([year, month, day]).daysInMonth();
    const daysInPreviousMonth = moment([year, month, day])
      .subtract(1, "month")
      .daysInMonth();
    let startDay = moment([year, month, 1]).day();

    const numPreviousDaysShown = Math.max(startDay - 1, 0);
    let numNextDaysShown =
      daysInWeek - ((daysInCurrentMonth + numPreviousDaysShown) % daysInWeek);
    if (numNextDaysShown === daysInWeek) {
      numNextDaysShown = 0;
    }

    let from = 0 - numPreviousDaysShown;
    let until = daysInCurrentMonth + numNextDaysShown;

    const classNamePrefix = "widget-day--";

    for (let i = from; i < until; i++) {
      let dayNumber = i + 1;
      const classNames = [];
      const style = {};
      let isDisabled = false;
      let type = "";
      let referenceDate = moment([year, month, 1]);

      if (dayNumber <= 0) {
        dayNumber = daysInPreviousMonth + i + 1;
        type = "previous";
        referenceDate.subtract(1, "month");
      } else if (dayNumber > daysInCurrentMonth) {
        dayNumber = i - daysInCurrentMonth + 1;
        type = "next";
        referenceDate.add(1, "month");
      } else {
        referenceDate.date(dayNumber);
      }

      // if (startDay) {
      //   style = { ...style, gridColumnStart: startDay};
      //   startDay = 0;
      // }

      if (type) {
        classNames.push(`${classNamePrefix}${type}`);
      }

      if (
        referenceDate.year() === selectedYear &&
        referenceDate.month() === selectedMonth &&
        dayNumber === selectedDay
      ) {
        classNames.push(`${classNamePrefix}selected`);
      } else if (
        dayNumber === day &&
        month === date.month() &&
        year === date.year()
      ) {
        classNames.push(`${classNamePrefix}current`);
      }

      if (
        this.checkIsInDisabledRange({
          day: dayNumber,
          month: referenceDate.month(),
          year: referenceDate.year()
        })
      ) {
        isDisabled = true;
        classNames.push(`${classNamePrefix}disabled`);
      }

      const clickAction = !isDisabled
        ? () =>
            this.changeDay({
              day: dayNumber,
              month: referenceDate.month(),
              year: referenceDate.year()
            })
        : () => {};

      list.push(
        <div
          className={`widget-day ${classNames.join(" ")}`}
          onClick={clickAction}
          style={style}
          key={i}
        >
          {dayNumber}
        </div>
      );
    }
    return list;
  }

  /**
   *  Disabled ranges = array of from, until
   *
   * @param {number} day
   * @param {number} month
   * @param {number} year
   */
  checkIsInDisabledRange({ day, month, year }: DatePropType) {
    const current = moment([year, month, day]);
    return (
      _find(this.props.disabledRanges, (range) => {
        const from = moment([
          range.from.year,
          range.from.month,
          range.from.day
        ]);
        const until = moment([
          range.until.year,
          range.until.month,
          range.until.day
        ]);
        return current.isSameOrAfter(from) && current.isSameOrBefore(until);
      }) != null
    );
  }

  createDayLabels() {
    const list = [];
    for (let i = 1; i < 8; i++) {
      const label = moment().day(i).format("ddd");
      list.push(
        <div className="widget-label" key={i}>
          {label}
        </div>
      );
    }
    return list;
  }

  createMonthSelector(current:any) {
    const list = [];
    for (let i = 0; i < 12; i++) {
      list.push({ value: i, label: moment().month(i).format("MMMM") });
    }
    return (
      <Dropdown
        onChange={(value) => this.changeMonth(value)}
        value={current}
        options={list}
        height={this.state.dropdownHeight}
      />
    );
  }

  createYearSelector(current:any) {
    const list = [];
    for (let i = MIN_YEAR; i < MAX_YEAR; i++) {
      list.push({ value: i, label: moment().year(i).format("YYYY") });
    }
    return (
      <Dropdown
        onChange={(value) => this.changeYear(value)}
        value={current}
        options={list}
        height={this.state.dropdownHeight}
      />
    );
  }

  changeDay({ day, month, year }:{ day:any, month:any, year:any }) {
    console.log(
      day,
      month,
      year,
      moment([year, month, day]).format("DD MM YYYY")
    );
      this.setState({selectedDate: moment([year, month, day]).format("DDMMYYYY")});
    // let { month, year } = this.state;
    // let reference = moment([year, month, 1]);
    // const day = value + 1;
    // if (value < 0) {
    //   reference.subtract(1, 'month');
    //   reference.date(reference.daysInMonth() + day);
    // }
    // else {
    //   reference.date(day);
    // }
    // console.log(reference.date(), month, year);

    // moving to previous or next
    this.setState({
      selectedDay: day,
      selectedMonth: month,
      selectedYear: year,
      month,
      year
    });
  }

  changeMonth(value:any ) {
    this.setState({ month: value });
  }

  changeYear(value:any) {
    this.setState({ year: value });
  }

  previousMonth() {
    let { day, month, year } = this.state;
    const previousMonth = moment([year, month, day]).add(-1, "month");
    this.setState({
      day: previousMonth.date(),
      month: previousMonth.month(),
      year: previousMonth.year()
    });
  }

  nextMonth() {
    let { day, month, year } = this.state;
    const nextMonth = moment([year, month, day]).add(1, "month");
    this.setState({
      day: nextMonth.date(),
      month: nextMonth.month(),
      year: nextMonth.year()
    });
  }

  componentDidUpdate() {
    if (this.state.dropdownHeight !== this.refs.body.clientHeight) {
      this.setState({ dropdownHeight: this.refs.body.clientHeight });
    }
  }
  componentDidMount() {
    this.setState({ dropdownHeight: this.refs.body.clientHeight });
  }

  // onChangeField({ value, day, month, year }) {
  //   console.log('change', value, day, month, year);
  //   if (day && month && year) {
  //     this.setState({ selectedDay: day, selectedMonth: month, selectedYear: year });
  //   }
  // }

  onChangeField({ day, month, year, resolvedDate }) {
    if (
      resolvedDate &&
      resolvedDate.isValid() &&
      resolvedDate.year() >= MIN_YEAR &&
      resolvedDate.year() <= MAX_YEAR
    ) {
      this.changeDay({ day, month, year });
    }
  }

  render() {
    const { month, year, selectedDate } = this.state;
    return (
      <div className="date-picker">
        <DateField onChange={(value) => this.onChangeField(value)} value={selectedDate} />
        <div className="widget">
          <div className="header">
            <MonthNavigator
              onClick={() => this.previousMonth()}
              direction="left"
            />
            <div className="header-month">
              {this.createMonthSelector(month)}
            </div>
            <div className="header-year">{this.createYearSelector(year)}</div>
            <MonthNavigator
              onClick={() => this.nextMonth()}
              direction="right"
            />
          </div>
          <div className="widget-body" ref="body">
            {this.createDayLabels()}
            {this.createDays()}
          </div>
        </div>
      </div>
    );
  }
}

完整回购 https://codesandbox.io/s/cold-night-nqvyy

1 个答案:

答案 0 :(得分:1)

您正在使用legacy string refs。您应该更新为较新的引用语法之一,explained here

DateField中,您有对输入元素的引用。

在构造函数中,调用createRef创建一个空的引用,并使用泛型告诉它我们将引用指向哪个DOM元素。

this.inputRef = createRef<HTMLInputElement>();

Typescript将给您一个错误“类型'DateField'上不存在属性'inputRef'”。因此,您需要声明该属性存在。将其添加到班级顶部。实际上,如果我们在这里拥有通用名称,那么我们就不需要在createRef中再说一次。

private readonly inputRef: React.RefObject<HTMLInputElement>;

要使用ref,请访问.current属性。我们使用?.运算符,因为如果尚未设置ref,则当前属性可能不存在。

handleDate(value: string) {
    const caretStart = this.inputRef.current?.selectionStart;
    const caretEnd = this.inputRef.current?.selectionEnd;
...

要将引用附加到input元素,请在render()函数中执行此操作

<input
    ....
    ref={this.inputRef}
/>

还有很多其他问题。我修复了部分here,但不是全部。