Repository: react-materialize/react-materialize
Pull request #1067 (2020-03-27 11:10:36)

refactored DatePicker to use hooks
TextInput also needed to change to forwardRef

This PR is mostly done by @DangerN 


Patch:
diff --git a/src/DatePicker.js b/src/DatePicker.js
index 146d6db..8c76b0a 100644
--- a/src/DatePicker.js
+++ b/src/DatePicker.js
@@ -1,31 +1,22 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
 import PropTypes from 'prop-types';
 import TextInput from './TextInput';
 import idgen from './idgen';
 
-class DatePicker extends React.Component {
-  componentDidMount() {
-    if (typeof M !== 'undefined') {
-      const { onChange, id } = this.props;
-      const elem = document.getElementById(id);
-      const options = onChange
-        ? { ...this.props.options, onSelect: onChange }
-        : this.props.options;
+const DatePicker = ({ options, onChange, ...rest }) => {
+  const dateEl = useRef(null);
 
-      this.instance = M.Datepicker.init(elem, options);
-    }
-  }
+  useEffect(() => {
+    const _options = onChange ? { ...options, onSelect: onChange } : options;
+    const instance = M.Datepicker.init(dateEl.current, _options);
 
-  componentWillUnmount() {
-    if (this.instance) {
-      this.instance.destroy();
-    }
-  }
+    return () => {
+      instance && instance.destroy();
+    };
+  }, [options, onChange]);
 
-  render() {
-    return <TextInput inputClassName="datepicker" {...this.props} />;
-  }
-}
+  return <TextInput ref={dateEl} inputClassName="datepicker" {...rest} />;
+};
 
 DatePicker.propTypes = {
   /**
diff --git a/src/Dropdown.js b/src/Dropdown.js
index 3d3272f..6905277 100644
--- a/src/Dropdown.js
+++ b/src/Dropdown.js
@@ -1,58 +1,77 @@
-import React, {
-  Fragment,
-  Children,
-  cloneElement,
-  useRef,
-  useEffect
-} from 'react';
+import React, { Component, Fragment, Children, cloneElement } from 'react';
 import PropTypes from 'prop-types';
 import idgen from './idgen';
 import cx from 'classnames';
 
-const Dropdown = ({ children, className, trigger, options, ...props }) => {
-  const _dropdownContent = useRef(null);
+class Dropdown extends Component {
+  constructor(props) {
+    super(props);
+    this.renderTrigger = this.renderTrigger.bind(this);
+    this.renderItems = this.renderItems.bind(this);
+  }
+
+  componentDidMount() {
+    const { options } = this.props;
+
+    if (typeof M !== undefined) {
+      this.instance = M.Dropdown.init(
+        document.querySelectorAll(this._trigger),
+        options
+      )[0];
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.instance) {
+      this.instance.destroy();
+    }
+  }
+
+  render() {
+    const { className, ...props } = this.props;
+    delete props.trigger;
+    delete props.options;
 
-  useEffect(() => {
-    const instance = M.Dropdown.init(
-      document.querySelector(`[data-target="${props.id}"]`),
-      options
+    return (
+      <Fragment>
+        {this.renderTrigger()}
+        <ul {...props} className={cx('dropdown-content', className)}>
+          {this.renderItems()}
+        </ul>
+      </Fragment>
     );
+  }
 
-    return () => {
-      instance && instance.destroy();
-    };
-  }, [options, props.id]);
+  renderTrigger() {
+    const { trigger, id } = this.props;
 
-  const renderTrigger = () =>
-    cloneElement(trigger, {
-      'data-target': props.id,
+    return cloneElement(trigger, {
+      'data-target': id,
+      ref: t => (this._trigger = `[data-target=${id}]`),
       className: cx(trigger.props.className, 'dropdown-trigger')
     });
+  }
 
-  const renderItems = () =>
-    Children.map(children, element => {
-      if (element.type.name === 'Divider') {
-        return <li key={idgen()} className="divider" tabIndex="-1" />;
-      } else {
-        return <li key={idgen()}>{element}</li>;
+  renderItems() {
+    const { children } = this.props;
+
+    return Children.map(children, element => {
+      if (element) {
+        if (element.type.name === 'Divider') {
+          return <li key={idgen()} className="divider" tabIndex="-1" />;
+        } else {
+          return <li key={idgen()}>{element}</li>;
+        }
       }
     });
-
-  return (
-    <Fragment>
-      {renderTrigger()}
-      <ul
-        {...props}
-        className={cx('dropdown-content', className)}
-        ref={_dropdownContent}
-      >
-        {renderItems()}
-      </ul>
-    </Fragment>
-  );
-};
+  }
+}
 
 Dropdown.propTypes = {
+  /*
+   * override id
+   * @default idgen()
+   */
   id: PropTypes.string,
   /**
    * The node to trigger the dropdown
@@ -83,7 +102,7 @@ Dropdown.propTypes = {
 };
 
 Dropdown.defaultProps = {
-  id: `Dropdown_${idgen()}`,
+  id: `Dropdown-${idgen()}`,
   options: {
     alignment: 'left',
     autoTrigger: true,
diff --git a/src/TextInput.js b/src/TextInput.js
index b45b879..b593232 100644
--- a/src/TextInput.js
+++ b/src/TextInput.js
@@ -1,18 +1,19 @@
-import React, { useRef, useEffect } from 'react';
+import React, { createRef, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import cx from 'classnames';
 import idgen from './idgen';
 import constants from './constants';
 
-const TextInput = props => {
-  const inputRef = useRef(null);
-
+const TextInput = React.forwardRef((props, ref) => {
   // eslint-disable-next-line react/prop-types
   const dataLength = props['data-length'];
+  const inputRef = ref || createRef(null);
 
   useEffect(() => {
-    dataLength && M.CharacterCounter.init(inputRef);
-  }, [dataLength]);
+    if (inputRef && dataLength) {
+      M.CharacterCounter.init(inputRef.current);
+    }
+  }, [dataLength, inputRef]);
 
   useEffect(() => {
     M.updateTextFields();
@@ -136,10 +137,11 @@ const TextInput = props => {
       {children}
     </div>
   );
-};
+});
+
+TextInput.displayName = 'TextInput';
 
 TextInput.propTypes = {
-  children: PropTypes.node,
   /*
    * Strip away all layout classes such as col and sX
    */
@@ -152,8 +154,8 @@ TextInput.propTypes = {
    * Responsive size for Tablet Devices
    */
   m: PropTypes.number,
-  /*
-   * Responsive size for Desktop Devices
+  /**
+   *  Responsive size for Desktop Devices
    */
   l: PropTypes.number,
   /**
@@ -220,7 +222,11 @@ TextInput.propTypes = {
   /*
    * email type
    */
-  email: PropTypes.bool
+  email: PropTypes.bool,
+  /*
+   * children
+   */
+  children: PropTypes.node
 };
 
 TextInput.defaultProps = {


Test patch:
diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml
index f9f9cca..f02ed8e 100644
--- a/.github/workflows/ci-test.yml
+++ b/.github/workflows/ci-test.yml
@@ -1,6 +1,9 @@
 name: Test
 
-on: [push]
+on:
+  push:
+    branches-ignore:
+      - master
 
 jobs:
   test:
diff --git a/test/DropdownButton.spec.js b/test/DropdownButton.spec.js
index 0552e68..1fa9c41 100644
--- a/test/DropdownButton.spec.js
+++ b/test/DropdownButton.spec.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import { shallow, mount } from 'enzyme';
+import { shallow } from 'enzyme';
 import Dropdown from '../src/Dropdown';
 import Divider from '../src/Divider';
 import mocker from './helper/new-mocker';
@@ -12,6 +12,13 @@ describe('<Dropdown />', () => {
     expect(wrapper).toMatchSnapshot();
   });
 
+  test('does not update ID reference with each render', () => {
+    wrapper = shallow(<Dropdown trigger={<span>hi</span>} />);
+    const { idx } = wrapper.instance();
+    wrapper.render();
+    expect(wrapper.instance().idx).toEqual(idx);
+  });
+
   test('does correctly render dividers', () => {
     wrapper = shallow(
       <Dropdown trigger={<span>hi</span>}>
@@ -36,7 +43,7 @@ describe('<Dropdown />', () => {
     const dropdownMock = {
       init: (el, options) => {
         dropdownInitMock(options);
-        return { destroy: dropdownInstanceDestroyMock };
+        return [{ destroy: dropdownInstanceDestroyMock }];
       }
     };
 
@@ -52,7 +59,7 @@ describe('<Dropdown />', () => {
     });
 
     test('with default options if none are given', () => {
-      wrapper = mount(<Dropdown trigger={<button>Drop me!</button>} />);
+      wrapper = shallow(<Dropdown trigger={<button>Drop me!</button>} />);
       expect(dropdownInitMock).toHaveBeenCalledWith({
         alignment: 'left',
         autoTrigger: true,
@@ -72,7 +79,7 @@ describe('<Dropdown />', () => {
 
     test('handles options prop', () => {
       const options = { constrainWidth: true };
-      wrapper = mount(
+      wrapper = shallow(
         <Dropdown trigger={<button>Drop me!</button>} options={options} />
       );
       expect(dropdownInitMock).toHaveBeenCalledWith(options);
@@ -80,7 +87,7 @@ describe('<Dropdown />', () => {
 
     test('should destroy when unmounted', () => {
       const options = { constrainWidth: true };
-      wrapper = mount(
+      wrapper = shallow(
         <Dropdown trigger={<button>Drop me!</button>} options={options} />
       );
       wrapper.unmount();
diff --git a/test/__snapshots__/DatePicker.spec.js.snap b/test/__snapshots__/DatePicker.spec.js.snap
index ef2887f..68a78bd 100644
--- a/test/__snapshots__/DatePicker.spec.js.snap
+++ b/test/__snapshots__/DatePicker.spec.js.snap
@@ -4,93 +4,6 @@ exports[`<DatePicker /> accepts external \`id\` value 1`] = `
 <TextInput
   id="ID"
   inputClassName="datepicker"
-  options={
-    Object {
-      "autoClose": false,
-      "container": null,
-      "defaultDate": null,
-      "disableDayFn": null,
-      "disableWeekends": false,
-      "events": Array [],
-      "firstDay": 0,
-      "format": "mmm dd, yyyy",
-      "i18n": Object {
-        "cancel": "Cancel",
-        "clear": "Clear",
-        "done": "Ok",
-        "months": Array [
-          "January",
-          "February",
-          "March",
-          "April",
-          "May",
-          "June",
-          "July",
-          "August",
-          "September",
-          "October",
-          "November",
-          "December",
-        ],
-        "monthsShort": Array [
-          "Jan",
-          "Feb",
-          "Mar",
-          "Apr",
-          "May",
-          "Jun",
-          "Jul",
-          "Aug",
-          "Sep",
-          "Oct",
-          "Nov",
-          "Dec",
-        ],
-        "nextMonth": "›",
-        "previousMonth": "‹",
-        "weekdays": Array [
-          "Sunday",
-          "Monday",
-          "Tuesday",
-          "Wednesday",
-          "Thursday",
-          "Friday",
-          "Saturday",
-        ],
-        "weekdaysAbbrev": Array [
-          "S",
-          "M",
-          "T",
-          "W",
-          "T",
-          "F",
-          "S",
-        ],
-        "weekdaysShort": Array [
-          "Sun",
-          "Mon",
-          "Tue",
-          "Wed",
-          "Thu",
-          "Fri",
-          "Sat",
-        ],
-      },
-      "isRTL": false,
-      "maxDate": null,
-      "minDate": null,
-      "onClose": null,
-      "onDraw": null,
-      "onOpen": null,
-      "onSelect": null,
-      "parse": null,
-      "setDefaultDate": false,
-      "showClearBtn": false,
-      "showDaysInNextAndPreviousMonths": false,
-      "showMonthAfterYear": false,
-      "yearRange": 10,
-    }
-  }
 />
 `;
 
@@ -98,93 +11,6 @@ exports[`<DatePicker /> renders 1`] = `
 <TextInput
   id="DatePicker-mockId"
   inputClassName="datepicker"
-  options={
-    Object {
-      "autoClose": false,
-      "container": null,
-      "defaultDate": null,
-      "disableDayFn": null,
-      "disableWeekends": false,
-      "events": Array [],
-      "firstDay": 0,
-      "format": "mmm dd, yyyy",
-      "i18n": Object {
-        "cancel": "Cancel",
-        "clear": "Clear",
-        "done": "Ok",
-        "months": Array [
-          "January",
-          "February",
-          "March",
-          "April",
-          "May",
-          "June",
-          "July",
-          "August",
-          "September",
-          "October",
-          "November",
-          "December",
-        ],
-        "monthsShort": Array [
-          "Jan",
-          "Feb",
-          "Mar",
-          "Apr",
-          "May",
-          "Jun",
-          "Jul",
-          "Aug",
-          "Sep",
-          "Oct",
-          "Nov",
-          "Dec",
-        ],
-        "nextMonth": "›",
-        "previousMonth": "‹",
-        "weekdays": Array [
-          "Sunday",
-          "Monday",
-          "Tuesday",
-          "Wednesday",
-          "Thursday",
-          "Friday",
-          "Saturday",
-        ],
-        "weekdaysAbbrev": Array [
-          "S",
-          "M",
-          "T",
-          "W",
-          "T",
-          "F",
-          "S",
-        ],
-        "weekdaysShort": Array [
-          "Sun",
-          "Mon",
-          "Tue",
-          "Wed",
-          "Thu",
-          "Fri",
-          "Sat",
-        ],
-      },
-      "isRTL": false,
-      "maxDate": null,
-      "minDate": null,
-      "onClose": null,
-      "onDraw": null,
-      "onOpen": null,
-      "onSelect": null,
-      "parse": null,
-      "setDefaultDate": false,
-      "showClearBtn": false,
-      "showDaysInNextAndPreviousMonths": false,
-      "showMonthAfterYear": false,
-      "yearRange": 10,
-    }
-  }
 />
 `;
 
@@ -193,92 +19,5 @@ exports[`<DatePicker /> renders with a label 1`] = `
   id="DatePicker-mockId"
   inputClassName="datepicker"
   label="DatePicker label"
-  options={
-    Object {
-      "autoClose": false,
-      "container": null,
-      "defaultDate": null,
-      "disableDayFn": null,
-      "disableWeekends": false,
-      "events": Array [],
-      "firstDay": 0,
-      "format": "mmm dd, yyyy",
-      "i18n": Object {
-        "cancel": "Cancel",
-        "clear": "Clear",
-        "done": "Ok",
-        "months": Array [
-          "January",
-          "February",
-          "March",
-          "April",
-          "May",
-          "June",
-          "July",
-          "August",
-          "September",
-          "October",
-          "November",
-          "December",
-        ],
-        "monthsShort": Array [
-          "Jan",
-          "Feb",
-          "Mar",
-          "Apr",
-          "May",
-          "Jun",
-          "Jul",
-          "Aug",
-          "Sep",
-          "Oct",
-          "Nov",
-          "Dec",
-        ],
-        "nextMonth": "›",
-        "previousMonth": "‹",
-        "weekdays": Array [
-          "Sunday",
-          "Monday",
-          "Tuesday",
-          "Wednesday",
-          "Thursday",
-          "Friday",
-          "Saturday",
-        ],
-        "weekdaysAbbrev": Array [
-          "S",
-          "M",
-          "T",
-          "W",
-          "T",
-          "F",
-          "S",
-        ],
-        "weekdaysShort": Array [
-          "Sun",
-          "Mon",
-          "Tue",
-          "Wed",
-          "Thu",
-          "Fri",
-          "Sat",
-        ],
-      },
-      "isRTL": false,
-      "maxDate": null,
-      "minDate": null,
-      "onClose": null,
-      "onDraw": null,
-      "onOpen": null,
-      "onSelect": null,
-      "parse": null,
-      "setDefaultDate": false,
-      "showClearBtn": false,
-      "showDaysInNextAndPreviousMonths": false,
-      "showMonthAfterYear": false,
-      "yearRange": 10,
-    }
-  }
 />
 `;
diff --git a/test/__snapshots__/DropdownButton.spec.js.snap b/test/__snapshots__/DropdownButton.spec.js.snap
index c0d330e..ddc34f0 100644
--- a/test/__snapshots__/DropdownButton.spec.js.snap
+++ b/test/__snapshots__/DropdownButton.spec.js.snap
@@ -4,13 +4,13 @@ exports[`<Dropdown /> renders 1`] = `
 <Fragment>
   <button
     className="dropdown-trigger"
-    data-target="Dropdown_mockId"
+    data-target="Dropdown-mockId"
   >
     Drop me!
   </button>
   <ul
     className="dropdown-content"
-    id="Dropdown_mockId"
+    id="Dropdown-mockId"
   />
 </Fragment>
 `;