Repository: react-materialize/react-materialize
Pull request #1070 (2020-03-27 12:34:53)

Autocomplete hooks
Continuation of #953 and #907 


Patch:
diff --git a/src/Autocomplete.js b/src/Autocomplete.js
index 2357a66..f8c824b 100644
--- a/src/Autocomplete.js
+++ b/src/Autocomplete.js
@@ -1,110 +1,52 @@
-/* eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
-import React, { Component } from 'react';
+import React, { cloneElement, useEffect, useRef } from 'react';
 import PropTypes from 'prop-types';
 import cx from 'classnames';
 import constants from './constants';
 import idgen from './idgen';
 
-class Autocomplete extends Component {
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      value: props.value || '',
-      itemSelected: false
-    };
-
-    this.renderIcon = this.renderIcon.bind(this);
-    this._onChange = this._onChange.bind(this);
-  }
-
-  componentDidMount() {
-    if (typeof M !== 'undefined') {
-      const { options } = this.props;
-      this.instance = M.Autocomplete.init(this._autocomplete, options);
-    }
-  }
-
-  componentWillUnmount() {
-    if (this.instance) {
-      this.instance.destroy();
-    }
-  }
-
-  renderIcon(icon) {
-    return React.cloneElement(icon, { className: 'prefix' });
-  }
-
-  _onChange(e) {
-    const { onChange } = this.props;
-    const value = e.target.value;
-
-    onChange && onChange(e, value);
-
-    this.setState({ value, itemSelected: false });
-  }
-
-  _onAutocomplete(value, e) {
-    const { onChange, options } = this.props;
-    const { onAutocomplete } = options;
-
-    onAutocomplete && onAutocomplete(value);
-
-    onChange && onChange(e, value);
-
-    this.setState({ value, itemSelected: true });
-  }
-
-  render() {
-    const {
-      id,
-      className,
-      title,
-      icon,
-      s,
-      m,
-      l,
-      xl,
-      offset,
-      placeholder,
-      // these are mentioned here only to prevent from getting into ...props
-      value,
-      onChange,
-      options,
-      ...props
-    } = this.props;
-
-    const sizes = { s, m, l, xl };
-    let classes = {
-      col: true
+const Autocomplete = ({
+  className,
+  title,
+  icon,
+  s,
+  m,
+  l,
+  xl,
+  id,
+  options,
+  ...rest
+}) => {
+  const autocompleteRef = useRef(null);
+  const sizes = { s, m, l, xl };
+
+  let classes = { col: true };
+
+  constants.SIZES.forEach(size => {
+    classes[size + sizes[size]] = sizes[size];
+  });
+
+  useEffect(() => {
+    const instance = M.Autocomplete.init(autocompleteRef.current, options);
+
+    return () => {
+      instance && instance.destroy();
     };
-    constants.SIZES.forEach(size => {
-      classes[size + sizes[size]] = sizes[size];
-    });
-
-    return (
-      <div
-        offset={offset}
-        className={cx('input-field', className, classes)}
-        {...props}
-      >
-        {icon && this.renderIcon(icon)}
-        <input
-          placeholder={placeholder}
-          className="autocomplete"
-          id={id}
-          onChange={this._onChange}
-          type="text"
-          value={this.state.value}
-          ref={input => {
-            this._autocomplete = input;
-          }}
-        />
-        <label htmlFor={id}>{title}</label>
-      </div>
-    );
-  }
-}
+  }, [options]);
+
+  return (
+    <div className={cx('input-field', className, classes)}>
+      {icon && cloneElement(icon, { className: 'prefix' })}
+      <input
+        className="autocomplete"
+        type="text"
+        ref={autocompleteRef}
+        id={id}
+        {...rest}
+      />
+      <label htmlFor={id}>{title}</label>
+    </div>
+  );
+};
 
 Autocomplete.propTypes = {
   /**
@@ -126,7 +68,6 @@ Autocomplete.propTypes = {
   m: PropTypes.number,
   l: PropTypes.number,
   xl: PropTypes.number,
-  offset: PropTypes.string,
   /**
    * Placeholder for input element
    * */
diff --git a/stories/Autocomplete.stories.js b/stories/Autocomplete.stories.js
index f7ff10a..b37bfe1 100644
--- a/stories/Autocomplete.stories.js
+++ b/stories/Autocomplete.stories.js
@@ -2,7 +2,7 @@ import React from 'react';
 import { storiesOf } from '@storybook/react';
 import Autocomplete from '../src/Autocomplete';
 import Icon from '../src/Icon';
-import { text, withKnobs } from '@storybook/addon-knobs';
+import { text, withKnobs, object } from '@storybook/addon-knobs';
 
 const stories = storiesOf('Javascript|Autocomplete', module);
 stories.addDecorator(withKnobs);
@@ -16,11 +16,11 @@ stories.add(
   () => (
     <Autocomplete
       options={{
-        data: {
+        data: object('Data', {
           ['Gus Fring']: null,
           ['Saul Goodman']: null,
           ['Tuco Salamanca']: 'https://placehold.it/250x250'
-        }
+        })
       }}
       placeholder="Insert here"
     />
@@ -33,11 +33,11 @@ stories.add(
   () => (
     <Autocomplete
       options={{
-        data: {
+        data: object('Data', {
           ['Gus Fring']: null,
           ['Saul Goodman']: null,
           ['Tuco Salamanca']: 'https://placehold.it/250x250'
-        }
+        })
       }}
       placeholder="Insert here"
       icon={<Icon>{text('icon', 'textsms')}</Icon>}


Test patch:
diff --git a/test/Autocomplete.spec.js b/test/Autocomplete.spec.js
index 22195f4..4c844fa 100644
--- a/test/Autocomplete.spec.js
+++ b/test/Autocomplete.spec.js
@@ -4,6 +4,7 @@ import Autocomplete from '../src/Autocomplete';
 import mocker from './helper/new-mocker';
 
 describe('<Autocomplete />', () => {
+  const autocompleteInitMock = jest.fn();
   const data = {
     Apple: null,
     Microsoft: null,
@@ -14,6 +15,10 @@ describe('<Autocomplete />', () => {
 
   let wrapper;
 
+  beforeEach(() => {
+    autocompleteInitMock.mockClear();
+  });
+
   test('renders', () => {
     wrapper = shallow(
       <Autocomplete title="Test Title" id={componentId} options={{ data }} />
@@ -35,7 +40,6 @@ describe('<Autocomplete />', () => {
   });
 
   describe('initialises', () => {
-    const autocompleteInitMock = jest.fn();
     const autocompleteInstanceDestroyMock = jest.fn();
     const autocompleteMock = {
       init: (el, options) => {
@@ -56,4 +60,32 @@ describe('<Autocomplete />', () => {
       expect(autocompleteInitMock).toHaveBeenCalledTimes(1);
     });
   });
+
+  describe('with new data', () => {
+    test('reinitializes', () => {
+      const wrapper = mount(<Autocomplete />);
+
+      expect(autocompleteInitMock).lastCalledWith({
+        data: {},
+        limit: Infinity,
+        minLength: 1,
+        onAutocomplete: null,
+        sortFunction: null
+      });
+
+      wrapper.setProps({
+        options: {
+          data: {
+            foo: 'bar'
+          }
+        }
+      });
+
+      expect(autocompleteInitMock).lastCalledWith({
+        data: {
+          foo: 'bar'
+        }
+      });
+    });
+  });
 });
diff --git a/test/__snapshots__/Autocomplete.spec.js.snap b/test/__snapshots__/Autocomplete.spec.js.snap
index 6fac4cc..f7481a4 100644
--- a/test/__snapshots__/Autocomplete.spec.js.snap
+++ b/test/__snapshots__/Autocomplete.spec.js.snap
@@ -7,9 +7,7 @@ exports[`<Autocomplete /> renders 1`] = `
   <input
     className="autocomplete"
     id="testAutocompleteId"
-    onChange={[Function]}
     type="text"
-    value=""
   />
   <label
     htmlFor="testAutocompleteId"