Repository: mlaursen/react-md
Pull request #800 (2018-09-20 23:40:29)

Release/1.6.x - enhancement: add custom date rendering
Extend DatePickerContainer and underlying components:
- getDateClassName - to provide custom css class for given date
- dateRenderer - for custom rendering of given date
- onPreviousMonth / onNextMonth / onYearSelected - to allow fetching of data on month/year switching
- added new props to ts definition (DatePickerContainer.d.ts)
- added example on http://localhost:8080/components/pickers/date - Custom Date Rendering Example

Source feature request: https://github.com/mlaursen/react-md/issues/799

![image](https://user-images.githubusercontent.com/5368643/45852119-e01a9900-bd46-11e8-9d77-b3703fa89e34.png)



Patch:
diff --git a/README.md b/README.md
index ff707c2e0a..a0a9a9f04a 100644
--- a/README.md
+++ b/README.md
@@ -80,14 +80,14 @@ Please see the list of [examples](examples/) for how you can get a project start
 <!DOCTYPE html>
 <html>
   <head>
-    <link rel="stylesheet" href="https://unpkg.com/react-md@1.6.0/dist/react-md.deep_purple-pink.min.css">
+    <link rel="stylesheet" href="https://unpkg.com/react-md@1.6.2/dist/react-md.deep_purple-pink.min.css">
     <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons|Roboto:400,500,700">
   </head>
   <body>
     <div id="app"></div>
     <script src="https://unpkg.com/react/dist/react-with-addons.min.js"></script>
     <script src="https://unpkg.com/react-dom/dist/react-dom.min.js"></script>
-    <script src="https://unpkg.com/react-md@1.6.0/dist/react-md.min.js"></script>
+    <script src="https://unpkg.com/react-md@1.6.2/dist/react-md.min.js"></script>
     <script>
     var MyAwesomeComponent = React.createClass({
       render: function()  {
diff --git a/docs/package.json b/docs/package.json
index bc055b927a..0f7e49b511 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -1,6 +1,6 @@
 {
   "name": "react-md-documentation",
-  "version": "1.6.1",
+  "version": "1.6.2",
   "description": "The documentaiton website for react-md",
   "homepage": "https://react-md.mlaursen.com",
   "private": true,
diff --git a/docs/src/components/Components/Pickers/Date/CustomDateRenderingExample.jsx b/docs/src/components/Components/Pickers/Date/CustomDateRenderingExample.jsx
new file mode 100644
index 0000000000..3c0f51de35
--- /dev/null
+++ b/docs/src/components/Components/Pickers/Date/CustomDateRenderingExample.jsx
@@ -0,0 +1,52 @@
+import React, { PureComponent } from 'react';
+import { DatePicker } from 'react-md';
+import './CustomDateRenderingExample.scss';
+
+export default class CustomDateRenderingExample extends PureComponent {
+
+  componentWillMount() {
+    const today = new Date();
+    const eventDay = today.getDate() > 15 ? today.getDate() - 2 : today.getDate() + 2;
+    this.eventDate = new Date(today.getFullYear(), today.getMonth(), eventDay);
+  }
+
+  getDateClassName = (date) => {
+    const isMonday = date.getDay() === 1;
+    if (this.isEventDate(date)) {
+      return 'day-with-events';
+    } else if (isMonday) {
+      return 'monday';
+    }
+    return null;
+  };
+
+  isEventDate = date =>
+    date.getFullYear() === this.eventDate.getFullYear()
+    && date.getMonth() === this.eventDate.getMonth()
+    && date.getDate() === this.eventDate.getDate();
+
+  dateRenderer = (date, day) => {
+    if (this.isEventDate(date)) {
+      return <span>{day}<sub>{5}</sub></span>;
+    }
+    return <span>{day}</span>;
+  };
+
+  render() {
+    return (
+      <DatePicker
+        className="inline-fixed-date-picker"
+        id="custom-date-rendering-date-picker"
+        pickerFooterClassName="hidden-footer"
+        displayMode="portrait"
+        label="Select a date"
+        textFieldStyle={{ display: 'none' }}
+        defaultVisible
+        inline
+        closeYearOnSelect
+        getDateClassName={this.getDateClassName}
+        dateRenderer={this.dateRenderer}
+      />
+    );
+  }
+}
diff --git a/docs/src/components/Components/Pickers/Date/CustomDateRenderingExample.scss b/docs/src/components/Components/Pickers/Date/CustomDateRenderingExample.scss
new file mode 100644
index 0000000000..1f073de939
--- /dev/null
+++ b/docs/src/components/Components/Pickers/Date/CustomDateRenderingExample.scss
@@ -0,0 +1,33 @@
+@import '~globals';
+
+@include react-md-pickers-force-orientation;
+
+.inline-fixed-date-picker {
+
+  .md-picker--inline {
+    position: relative;
+    margin: auto;
+    left: 0;
+    z-index: inherit;
+  }
+
+  .hidden-footer {
+    display: none;
+  }
+
+  .monday {
+    background-color: $md-light-blue-50;
+    margin-bottom: 3px !important;
+    height: 36px !important;
+    width: $md-date-picker-portrait-ball-size;
+    border-radius: 50%;
+  }
+
+  .day-with-events {
+    background-color: $md-teal-a-200;
+    margin-bottom: 3px !important;
+    height: 36px !important;
+    width: $md-date-picker-portrait-ball-size;
+    border-radius: 50%;
+  }
+}
\ No newline at end of file
diff --git a/docs/src/components/Components/Pickers/Date/index.jsx b/docs/src/components/Components/Pickers/Date/index.jsx
index 5cc6ecedc9..28eca5dc4e 100644
--- a/docs/src/components/Components/Pickers/Date/index.jsx
+++ b/docs/src/components/Components/Pickers/Date/index.jsx
@@ -6,6 +6,8 @@ import OrientationExamples from './OrientationExamples';
 import OrientationExamplesRaw from '!!raw-loader!./OrientationExamples.jsx';
 import InlineExample from './InlineExample';
 import InlineExampleRaw from '!!raw-loader!./InlineExample.jsx';
+import CustomDateRenderingExample from './CustomDateRenderingExample';
+import CustomDateRenderingExampleRaw from '!!raw-loader!./CustomDateRenderingExample.jsx';
 import AdditionalDisplaySettings from './AdditionalDisplaySettings';
 import AdditionalDisplaySettingsRaw from '!!raw-loader!./AdditionalDisplaySettings.jsx';
 import Formatting from './Formatting';
@@ -38,6 +40,13 @@ providing the \`inline\` prop to the picker.
   `,
   code: InlineExampleRaw,
   children: <InlineExample />,
+}, {
+  title: 'Custom Date Rendering Example',
+  description: `
+DatePicker can be used as static month calendar with custom date rendering.
+  `,
+  code: CustomDateRenderingExampleRaw,
+  children: <CustomDateRenderingExample />,
 }, {
   title: 'Additional Display Settings',
   description: `
diff --git a/docs/src/components/DiscoverMore/WhatsNew/README.md b/docs/src/components/DiscoverMore/WhatsNew/README.md
index 33e0d4a4b3..72f82b6351 100644
--- a/docs/src/components/DiscoverMore/WhatsNew/README.md
+++ b/docs/src/components/DiscoverMore/WhatsNew/README.md
@@ -1,4 +1,8 @@
 ## September 2018
+### v1.6.2 Released
+- Updated the `InkContainer` to not break in Preact. [issues-788]
+- Updated the `TableRow` children PropType validation to be a bit more accurate. [commit-5e8d216]
+
 ### v1.6.1 Released
 ##### Changelog
 - Fixed the `DatePicker` dates not having `type="button"`. ([@jakerichan] - [pull-796])
diff --git a/package.json b/package.json
index 7a786b8e0a..c4e6e13b28 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "react-md",
-  "version": "1.6.1",
+  "version": "1.6.2",
   "description": "React material design components",
   "main": "./lib",
   "module": "./es",
diff --git a/src/js/DataTables/TableRow.d.ts b/src/js/DataTables/TableRow.d.ts
index 964df64526..c354154628 100644
--- a/src/js/DataTables/TableRow.d.ts
+++ b/src/js/DataTables/TableRow.d.ts
@@ -2,7 +2,7 @@ import * as React from 'react';
 import { Props } from '../index';
 
 export interface TableRowProps extends Props {
-  children?: Array<React.ReactElement<any>> | React.ReactElement<any>;
+  children?: Array<React.ReactElement<any>> | React.ReactElement<any> | React.ReactNode;
   onCheckboxClick?: (rowIndex: number, checked: boolean, event: React.MouseEvent<HTMLTableRowElement>) => void;
   selected?: boolean;
   selectable?: boolean;
diff --git a/src/js/DataTables/TableRow.js b/src/js/DataTables/TableRow.js
index 9d96fdfa4c..b7dfc705e9 100644
--- a/src/js/DataTables/TableRow.js
+++ b/src/js/DataTables/TableRow.js
@@ -34,6 +34,7 @@ export default class TableRow extends Component {
     children: PropTypes.oneOfType([
       PropTypes.element,
       PropTypes.arrayOf(PropTypes.element),
+      PropTypes.node,
     ]).isRequired,
 
     /**
diff --git a/src/js/Inks/InkContainer.js b/src/js/Inks/InkContainer.js
index ccc3fb8a4d..33860c16a6 100644
--- a/src/js/Inks/InkContainer.js
+++ b/src/js/Inks/InkContainer.js
@@ -62,6 +62,10 @@ export default class InkContainer extends PureComponent {
       clearTimeout(this._removeTimeout);
     }
 
+    if (this.timeout) {
+      clearTimeout(this.timeout);
+    }
+
     if (this._container) {
       this._initOrRemoveEvents({ disabledInteractions: ['keyboard', 'mouse', 'touch'] });
       this._getKeyboardContainer().removeEventListener('blur', this._handleBlur);
@@ -179,12 +183,17 @@ export default class InkContainer extends PureComponent {
    */
   _setContainers = (group) => {
     if (group !== null) {
-      this._inkContainer = findDOMNode(group);
-      this._container = this._inkContainer.parentElement;
-
-      if (this._container) {
-        this._initOrRemoveEvents(this.props);
-      }
+      // temp workaround for preact
+      // https://github.com/mlaursen/react-md/issues/788#issuecomment-419248903
+      this.timeout = setTimeout(() => {
+        this.timeout = undefined;
+        this._inkContainer = findDOMNode(group);
+        this._container = this._inkContainer.parentElement;
+
+        if (this._container) {
+          this._initOrRemoveEvents(this.props);
+        }
+      });
     }
   };
 
diff --git a/src/js/Pickers/CalendarDate.js b/src/js/Pickers/CalendarDate.js
index a3bd810672..0402aa7205 100644
--- a/src/js/Pickers/CalendarDate.js
+++ b/src/js/Pickers/CalendarDate.js
@@ -20,6 +20,8 @@ export default class CalendarDate extends PureComponent {
     onClick: PropTypes.func.isRequired,
     active: PropTypes.bool,
     today: PropTypes.bool,
+    getDateClassName: PropTypes.func,
+    dateRenderer: PropTypes.func,
   };
 
   constructor(props) {
@@ -65,9 +67,14 @@ export default class CalendarDate extends PureComponent {
 
   render() {
     const { date, desktopActive } = this.state;
-    const { disabled, active, today, className } = this.props;
+    const { disabled, active, today, className, getDateClassName, dateRenderer } = this.props;
 
     const fullyActive = today && !active && !desktopActive;
+
+    const generatedClassName = getDateClassName
+      ? getDateClassName(this.props.date, date)
+      : null;
+
     return (
       <div
         role="gridcell"
@@ -84,7 +91,7 @@ export default class CalendarDate extends PureComponent {
           className={cn('md-calendar-date--btn', {
             'md-calendar-date--btn-active': active || desktopActive,
             'md-pointer--hover': !disabled,
-          }, themeColors({ disabled, primary: fullyActive }), 'md-btn', className)}
+          }, themeColors({ disabled, primary: fullyActive }), 'md-btn', className, generatedClassName)}
           onClick={this._handleClick}
           disabled={disabled}
         >
@@ -94,7 +101,7 @@ export default class CalendarDate extends PureComponent {
               'md-font-bold': fullyActive,
             })}
           >
-            {date}
+            {dateRenderer ? dateRenderer(this.props.date, date) : date}
           </span>
         </button>
       </div>
diff --git a/src/js/Pickers/CalendarMonth.js b/src/js/Pickers/CalendarMonth.js
index 5e61731753..eb5557ba5b 100644
--- a/src/js/Pickers/CalendarMonth.js
+++ b/src/js/Pickers/CalendarMonth.js
@@ -64,6 +64,16 @@ export default class CalendarMonth extends PureComponent {
      */
     dateClassName: PropTypes.string,
 
+    /**
+     * An optional function to provide class for each date in calendar.
+     */
+    getDateClassName: PropTypes.func,
+
+      /**
+     * An optional function to render each date component.
+     */
+    dateRenderer: PropTypes.func,
+
     /**
      * True if dates from adjacent months should be shown.
      */
@@ -98,6 +108,8 @@ export default class CalendarMonth extends PureComponent {
       firstDayOfWeek,
       disableWeekEnds,
       dateClassName,
+      getDateClassName,
+      dateRenderer,
       showAllDays,
       outerDateClassName,
       disableOuterDates,
@@ -138,6 +150,8 @@ export default class CalendarMonth extends PureComponent {
             key={key}
             className={cn(dateClassName, { [outerDateClassName]: !currentMonth && outerDateClassName })}
             today={time === todayTime}
+            getDateClassName={getDateClassName}
+            dateRenderer={dateRenderer}
             active={time === activeDateTime}
             disabled={disabled}
             onClick={onCalendarDateClick}
diff --git a/src/js/Pickers/DatePicker.js b/src/js/Pickers/DatePicker.js
index 10ae5c6e95..a6ac9b18cb 100644
--- a/src/js/Pickers/DatePicker.js
+++ b/src/js/Pickers/DatePicker.js
@@ -81,6 +81,16 @@ export default class DatePicker extends PureComponent {
      */
     calendarOuterDateClassName: PropTypes.string,
 
+    /**
+     * An optional function to provide class for each date in calendar.
+     */
+    getDateClassName: PropTypes.func,
+
+    /**
+     * An optional function to render each date component.
+     */
+    dateRenderer: PropTypes.func,
+
     /**
      * An optional className to apply to the title in calendar header.
      */
@@ -135,6 +145,8 @@ export default class DatePicker extends PureComponent {
       yearPickerClassName,
       calendarDateClassName,
       calendarOuterDateClassName,
+      getDateClassName,
+      dateRenderer,
       calendarTitleClassName,
       calendarTitleFormat,
       calendarWeekdayClassName,
@@ -157,6 +169,8 @@ export default class DatePicker extends PureComponent {
           showAllDays={showAllDays}
           disableOuterDates={disableOuterDates}
           dateClassName={calendarDateClassName}
+          getDateClassName={getDateClassName}
+          dateRenderer={dateRenderer}
           outerDateClassName={calendarOuterDateClassName}
           titleClassName={calendarTitleClassName}
           titleFormat={calendarTitleFormat}
diff --git a/src/js/Pickers/DatePickerCalendar.js b/src/js/Pickers/DatePickerCalendar.js
index 537e0c57fc..b5922fe33f 100644
--- a/src/js/Pickers/DatePickerCalendar.js
+++ b/src/js/Pickers/DatePickerCalendar.js
@@ -44,6 +44,16 @@ export default class DatePickerCalendar extends PureComponent {
      */
     dateClassName: PropTypes.string,
 
+    /**
+     * An optional function to provide class for each date in calendar.
+     */
+    getDateClassName: PropTypes.func,
+
+    /**
+     * An optional function to render each date component.
+     */
+    dateRenderer: PropTypes.func,
+
     /**
      * An optional className to apply to a date from an adjacent month.
      */
@@ -93,6 +103,8 @@ export default class DatePickerCalendar extends PureComponent {
       showAllDays,
       disableOuterDates,
       dateClassName,
+      getDateClassName,
+      dateRenderer,
       outerDateClassName,
       titleClassName,
       titleFormat,
@@ -132,6 +144,8 @@ export default class DatePickerCalendar extends PureComponent {
           showAllDays={showAllDays}
           disableOuterDates={disableOuterDates}
           dateClassName={dateClassName}
+          getDateClassName={getDateClassName}
+          dateRenderer={dateRenderer}
           outerDateClassName={outerDateClassName}
         />
       </section>
diff --git a/src/js/Pickers/DatePickerContainer.d.ts b/src/js/Pickers/DatePickerContainer.d.ts
index 97fd7100be..a4cefa4858 100644
--- a/src/js/Pickers/DatePickerContainer.d.ts
+++ b/src/js/Pickers/DatePickerContainer.d.ts
@@ -18,6 +18,11 @@ export interface DatePickerProps extends BasePickerProps {
   yearPickerClassName?: string;
   calendarDateClassName?: string;
   calendarOuterDateClassName?: string;
+  getDateClassName?: (date: Date, day: number) => string;
+  dateRenderer?: (date: Date, day: number) => React.ReactElement<any>;
+  onPreviousMonth?: (firstDayInMonth: Date) => void;
+  onNextMonth?: (firstDayInMonth: Date) => void;
+  onYearSelected?: (firstDayInYear: Date) => void;
   calendarTitleClassName?: string;
   calendarTitleFormat?: CalendarTitleFormat;
   calendarWeekdayClassName?: string;
diff --git a/src/js/Pickers/DatePickerContainer.js b/src/js/Pickers/DatePickerContainer.js
index fa29e01ca3..b1534730b3 100644
--- a/src/js/Pickers/DatePickerContainer.js
+++ b/src/js/Pickers/DatePickerContainer.js
@@ -467,6 +467,51 @@ export default class DatePickerContainer extends PureComponent {
      */
     calendarOuterDateClassName: PropTypes.string,
 
+    /**
+     * An optional function to provide class for each date in calendar.
+     *
+     * ```js
+     * getDateClassName(date:Date, day:number): string;
+     * ```
+     */
+    getDateClassName: PropTypes.func,
+
+    /**
+     * An optional function to render each date component.
+     *
+     * ```js
+     * dateRenderer(date:Date, day:number): React.Component;
+     * ```
+     */
+    dateRenderer: PropTypes.func,
+
+    /**
+     * An optional callback triggered on previous month click.
+     *
+     * ```js
+     * onPreviousMonth(firstDayInMonth:Date);
+     * ```
+     */
+    onPreviousMonth: PropTypes.func,
+
+    /**
+     * An optional callback triggered on next month click.
+     *
+     * ```js
+     * onNextMonth(firstDayInMonth:Date);
+     * ```
+     */
+    onNextMonth: PropTypes.func,
+
+    /**
+     * An optional callback triggered on year selection.
+     *
+     * ```js
+     * onYearSelected(firstDayInYear:Date);
+     * ```
+     */
+    onYearSelected: PropTypes.func,
+
     /**
      * An optional className to apply to the title in calendar header.
      */
@@ -754,11 +799,19 @@ export default class DatePickerContainer extends PureComponent {
   _previousMonth = () => {
     const calendarDate = addDate(this.state.calendarDate, -1, 'M');
     this.setState({ calendarDate });
+
+    if (this.props.onPreviousMonth) {
+      this.props.onPreviousMonth(calendarDate);
+    }
   };
 
   _nextMonth = () => {
     const calendarDate = addDate(this.state.calendarDate, 1, 'M');
     this.setState({ calendarDate });
+
+    if (this.props.onNextMonth) {
+      this.props.onNextMonth(calendarDate);
+    }
   };
 
   _setCalendarTempDate = (calendarTempDate) => {
@@ -808,6 +861,10 @@ export default class DatePickerContainer extends PureComponent {
       nextTemp = new Date(maxDate);
     }
 
+    if (this.props.onYearSelected) {
+      this.props.onYearSelected(nextDate);
+    }
+
     this.setState({
       calendarDate: nextDate,
       calendarTempDate: nextTemp,
@@ -923,6 +980,8 @@ export default class DatePickerContainer extends PureComponent {
       readOnly,
       onVisibilityChange,
       defaultCalendarDate,
+      getDateClassName,
+      dateRenderer,
 
       // deprecated
       initialCalendarDate,
@@ -959,6 +1018,8 @@ export default class DatePickerContainer extends PureComponent {
         onNextClick={this._nextMonth}
         onCalendarDateClick={this._setCalendarTempDate}
         onCalendarYearClick={this._setCalendarTempYear}
+        getDateClassName={getDateClassName}
+        dateRenderer={dateRenderer}
       />
     );
 
diff --git a/src/js/Version.js b/src/js/Version.js
index 0527ceb03c..aab4a82061 100644
--- a/src/js/Version.js
+++ b/src/js/Version.js
@@ -1,1 +1,1 @@
-export default '1.6.1';
+export default '1.6.2';


Test patch:
diff --git a/src/js/DataTables/__tests__/TableRow.js b/src/js/DataTables/__tests__/TableRow.js
index ff17dab5fd..b004e9e42a 100644
--- a/src/js/DataTables/__tests__/TableRow.js
+++ b/src/js/DataTables/__tests__/TableRow.js
@@ -231,4 +231,40 @@ describe('TableRow', () => {
       expect(table.state.selectedRows).toEqual([false, true]);
     });
   });
+
+  describe('rendering', () => {
+    global.captureConsole();
+
+    it(
+      'should not throw a PropType validation warning when the children prop is a ' +
+        'combination of a const element and a list of elements',
+      () => {
+        // eslint-disable-next-line react/prop-types
+        const Example = ({ children }) => (
+          <DataTable baseId="test">
+            <TableBody>{children}</TableBody>
+          </DataTable>
+        );
+
+        const list1 = [];
+        const list2 = [{ key: '1', children: 'Hello' }];
+
+        mount((
+          <Example>
+            <TableRow>
+              <TableColumn>Hi</TableColumn>
+              {list1.map((props) => <TableColumn {...props} />)}
+            </TableRow>
+          </Example>
+        ));
+        mount((
+          <Example>
+            <TableRow>
+              <TableColumn>Hi</TableColumn>
+              {list2.map((props) => <TableColumn {...props} />)}
+            </TableRow>
+          </Example>
+        ));
+      });
+  });
 });