Repository: react-materialize/react-materialize
Pull request #852 (2019-04-26 19:42:11)

Custom error/success messages for Textarea
# Description

As per the title.

As a side note:

@alextrastero, `Textarea` and `TextInput` components shares really a lot of code.
We should find a way to refactor these two components, what do you think?
We can, at least, move `renderIcons`, `renderLabel`, etc functions in their own files and then import them in both components.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

# How Has This Been Tested?

Tested it in a side project and through Storybook

# Checklist:

- [x] I have commented my code, particularly in hard-to-understand areas
- [x] My changes generate no new warnings
- [x] I have not generated a new package version. (the maintainers will handle that)



Patch:
diff --git a/src/Textarea.js b/src/Textarea.js
index 7d26aee..f22907c 100644
--- a/src/Textarea.js
+++ b/src/Textarea.js
@@ -6,36 +6,43 @@ import Icon from './Icon';
 import constants from './constants';
 
 class Textarea extends Component {
-  componentDidUpdate() {
-    if (typeof M !== 'undefined') {
-      M.textareaAutoResize(this._textarea);
-    }
+  constructor(props) {
+    super(props);
+
+    this.id = props.id || idgen();
   }
 
-  renderIcon(icon, className) {
-    return (
-      <Icon className={cx('material-icons prefix', className)}>{icon}</Icon>
-    );
+  componentDidUpdate(prevProps) {
+    const { value } = this.props;
+
+    if (typeof M !== 'undefined' && value !== prevProps.value) {
+      M.textareaAutoResize(this._textarea);
+    }
   }
 
   render() {
     const {
+      children,
       className,
       s,
       m,
       l,
       xl,
+      disabled,
       noLayout,
-      iconClassName,
+      placeholder,
       icon,
       label,
-      id,
+      success,
+      error,
+      iconClassName,
       onChange,
+      validate,
+      defaultValue,
+      value,
       ...other
     } = this.props;
 
-    const computedId = id || idgen();
-
     const sizes = { s, m, l, xl };
 
     let responsiveClasses;
@@ -48,25 +55,70 @@ class Textarea extends Component {
 
     const wrapperClasses = cx('input-field', responsiveClasses);
 
+    const textareaProps = {
+      placeholder,
+      id: this.id,
+      value,
+      defaultValue,
+      disabled,
+      ...other
+    };
+
+    const renderLabel = () =>
+      label && (
+        <label
+          className={cx({
+            active: value || placeholder,
+            'label-icon': typeof label !== 'string'
+          })}
+          data-success={success}
+          data-error={error}
+          htmlFor={this.id}
+        >
+          {label}
+        </label>
+      );
+
+    const renderHelper = () =>
+      [error || success] && (
+        <span
+          className="helper-text"
+          data-error={error}
+          data-success={success}
+        />
+      );
+
+    const renderIcon = () => {
+      if (!icon) return;
+
+      if (typeof icon === 'string') {
+        return <Icon className="prefix">{icon}</Icon>;
+      }
+
+      return React.cloneElement(icon, { className: 'prefix' });
+    };
+
     return (
       <div className={wrapperClasses}>
-        {icon && this.renderIcon(icon, iconClassName)}
+        {renderIcon()}
         <textarea
-          {...other}
-          ref={input => {
-            this._textarea = input;
+          {...textareaProps}
+          ref={textarea => {
+            this._textarea = textarea;
           }}
           onChange={onChange}
-          id={computedId}
-          className={cx('materialize-textarea', className)}
+          className={cx('materialize-textarea', { validate }, className)}
         />
-        <label htmlFor={computedId}>{label}</label>
+        {renderLabel()}
+        {renderHelper()}
+        {children}
       </div>
     );
   }
 }
 
 Textarea.propTypes = {
+  children: PropTypes.node,
   /*
    * Strip away all layout classes such as col and sX
    */
@@ -87,6 +139,19 @@ Textarea.propTypes = {
    *  Responsive size for Large Desktop Devices
    */
   xl: PropTypes.number,
+  /*
+   * disabled input
+   */
+  disabled: PropTypes.bool,
+  /*
+   * Placeholder string
+   */
+  placeholder: PropTypes.string,
+  /*
+   * override id
+   * @default idgen()
+   */
+  id: PropTypes.string,
   /*
    * render icon next to input
    */
@@ -100,26 +165,33 @@ Textarea.propTypes = {
    */
   label: PropTypes.string,
   /*
-   * onChange callback
+   * Input initial value
    */
-  onChange: PropTypes.func,
+  defaultValue: PropTypes.string,
   /*
-   * override id
-   * @default idgen()
+   * predefined value
    */
-  id: PropTypes.string,
+  value: PropTypes.string,
   /*
-   * disabled input
+   * Add validate class to input
    */
-  disabled: PropTypes.bool,
+  validate: PropTypes.bool,
   /*
-   * predefined value
+   * Custom success message
    */
-  value: PropTypes.string,
+  success: PropTypes.string,
+  /*
+   * Custom error message
+   */
+  error: PropTypes.string,
   /*
    * textarea classname
    */
-  className: PropTypes.string
+  className: PropTypes.string,
+  /*
+   * onChange callback
+   */
+  onChange: PropTypes.func
 };
 
 export default Textarea;


Test patch:
diff --git a/test/__snapshots__/Textarea.spec.js.snap b/test/__snapshots__/Textarea.spec.js.snap
index 83f3b5f..d1a0d30 100644
--- a/test/__snapshots__/Textarea.spec.js.snap
+++ b/test/__snapshots__/Textarea.spec.js.snap
@@ -10,8 +10,8 @@ exports[`<Textarea /> Disabled 1`] = `
     id="mockId"
     value="I was here"
   />
-  <label
-    htmlFor="mockId"
+  <span
+    className="helper-text"
   />
 </div>
 `;
@@ -21,7 +21,7 @@ exports[`<Textarea /> Icon 1`] = `
   className="input-field col"
 >
   <Icon
-    className="material-icons prefix red"
+    className="prefix"
   >
     mode_edit
   </Icon>
@@ -29,8 +29,8 @@ exports[`<Textarea /> Icon 1`] = `
     className="materialize-textarea"
     id="mockId"
   />
-  <label
-    htmlFor="mockId"
+  <span
+    className="helper-text"
   />
 </div>
 `;
@@ -44,10 +44,14 @@ exports[`<Textarea /> Label 1`] = `
     id="mockId"
   />
   <label
+    className=""
     htmlFor="mockId"
   >
     Write something here...
   </label>
+  <span
+    className="helper-text"
+  />
 </div>
 `;
 
@@ -60,8 +64,8 @@ exports[`<Textarea /> Placeholder 1`] = `
     id="mockId"
     placeholder="I was here"
   />
-  <label
-    htmlFor="mockId"
+  <span
+    className="helper-text"
   />
 </div>
 `;
@@ -74,8 +78,8 @@ exports[`<Textarea /> Textarea 1`] = `
     className="materialize-textarea"
     id="mockId"
   />
-  <label
-    htmlFor="mockId"
+  <span
+    className="helper-text"
   />
 </div>
 `;