From 615c3148a1f6d4d10d451dfe232b16af1ce56035 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Thu, 22 Oct 2015 12:43:31 +0200 Subject: [PATCH 1/2] [List] Implement SelectableList using a HOC --- docs/package.json | 2 + docs/src/app/components/markdown-element.jsx | 41 ++++++ .../app/components/pages/components/lists.jsx | 43 ++++++ .../app/components/raw-code/lists-code.txt | 9 ++ docs/webpack-dev-server.config.js | 4 + docs/webpack-production.config.js | 4 + src/hoc/selectable-enhance.js | 123 ++++++++++++++++++ src/index.js | 1 + 8 files changed, 227 insertions(+) create mode 100644 docs/src/app/components/markdown-element.jsx create mode 100644 src/hoc/selectable-enhance.js diff --git a/docs/package.json b/docs/package.json index da50229a91f868..9ab20265d8eead 100644 --- a/docs/package.json +++ b/docs/package.json @@ -15,7 +15,9 @@ }, "dependencies": { "codemirror": "^5.5.0", + "github-markdown-css": "^2.1.0", "history": "^1.11.1", + "marked": "^0.3.5", "react-addons-perf": "^0.14.0", "react-dom": "^0.14.0", "react-motion": "^0.3.1", diff --git a/docs/src/app/components/markdown-element.jsx b/docs/src/app/components/markdown-element.jsx new file mode 100644 index 00000000000000..e9b689547ceff3 --- /dev/null +++ b/docs/src/app/components/markdown-element.jsx @@ -0,0 +1,41 @@ +const React = require('react'); +let { Paper } = require('material-ui'); +import marked from 'marked'; + +const MarkdownElement = React.createClass({ + propTypes: { + text: React.PropTypes.string.isRequired + }, + getDefaultProps() { + return { + text: '' + }; + }, + + + componentWillMount() { + marked.setOptions({ + gfm: true, + tables: true, + breaks: false, + pedantic: false, + sanitize: true, + smartLists: true, + smartypants: false + }); + }, + + render() { + const { text } = this.props, + html = marked(text || ''); + + return ( + +
+
+
+ ); + }, +}); + +module.exports = MarkdownElement; diff --git a/docs/src/app/components/pages/components/lists.jsx b/docs/src/app/components/pages/components/lists.jsx index c6a7222f91a759..aa42c1c74eab3b 100644 --- a/docs/src/app/components/pages/components/lists.jsx +++ b/docs/src/app/components/pages/components/lists.jsx @@ -39,6 +39,13 @@ export default class ListsPage extends React.Component { constructor(props) { super(props); + this.state = { selectedIndex: 1 } + } + + handleUpdateSelectedIndex = (index) => { + this.setState({ + selectedIndex: index, + }); } render() { @@ -71,6 +78,13 @@ export default class ListsPage extends React.Component { header: 'optional', desc: 'The style object to override subheader styles.', }, + { + name: 'selectedLink', + type: 'valueLink', + header: 'optional', + desc: 'Makes List controllable. Highlights the ListItem whose index prop matches this "selectedLink.value". ' + + '"selectedLink.requestChange" represents a callback function to change that value (e.g. in state).', + }, ], }, { @@ -94,6 +108,13 @@ export default class ListsPage extends React.Component { header: 'default: false', desc: 'If true, the children will be indented by 72px. Only needed if there is no left avatar or left icon.', }, + { + name: 'index', + type: 'number', + header: 'optional', + desc: 'If selectedLink prop is passed to List component, this index prop is also required. It assigns a number ' + + 'to the tab so that it can be hightlighted by the List.', + }, { name: 'leftAvatar', type: 'element', @@ -666,6 +687,28 @@ export default class ListsPage extends React.Component { secondaryTextLines={2} /> + + + } /> + } /> + } /> + } /> + } /> + + ); diff --git a/docs/src/app/components/raw-code/lists-code.txt b/docs/src/app/components/raw-code/lists-code.txt index ef8c91e46c219d..1935c3ad50fbf0 100644 --- a/docs/src/app/components/raw-code/lists-code.txt +++ b/docs/src/app/components/raw-code/lists-code.txt @@ -97,3 +97,12 @@ ]} /> + +// List with selected indicator + + + + + diff --git a/docs/webpack-dev-server.config.js b/docs/webpack-dev-server.config.js index 800c7991bfdffa..050a66ec10931e 100644 --- a/docs/webpack-dev-server.config.js +++ b/docs/webpack-dev-server.config.js @@ -83,6 +83,10 @@ var config = { loader:'babel-loader?optional=runtime&stage=0', //react-hot is like browser sync and babel loads jsx and es6-7 include: [__dirname, path.resolve(__dirname, '../src')], //include these files exclude: [nodeModulesPath] //exclude node_modules so that they are not all compiled + }, + { + test: /\.css$/, // Only .css files + loader: 'style!css' // Run both loaders } ] }, diff --git a/docs/webpack-production.config.js b/docs/webpack-production.config.js index e742dec2f3c6de..dcb31e9cfe18f2 100644 --- a/docs/webpack-production.config.js +++ b/docs/webpack-production.config.js @@ -85,6 +85,10 @@ var config = { loader: 'raw-loader', include: path.resolve(__dirname, 'src/app/components/raw-code') }, + { + test: /\.css$/, // Only .css files + loader: 'style!css' // Run both loaders + }, ] }, eslint: { diff --git a/src/hoc/selectable-enhance.js b/src/hoc/selectable-enhance.js new file mode 100644 index 00000000000000..ae799264bacd28 --- /dev/null +++ b/src/hoc/selectable-enhance.js @@ -0,0 +1,123 @@ +/*eslint-disable */ +const React = require('react'); +const ThemeManager = require('../styles/theme-manager'); +const StylePropable = require('../mixins/style-propable'); +const ColorManipulator = require('../utils/color-manipulator'); +const DefaultRawTheme = require('../styles/raw-themes/light-raw-theme'); + +export var SelectableContainerEnhance = (Component) => { // eslint-disable-line no-var + let composed = React.createClass({ + + mixins: [StylePropable], + + contextTypes: { + muiTheme: React.PropTypes.object, + }, + + displayName: `Selectable${Component.displayName}`, + + propTypes: { + valueLink: React.PropTypes.shape({ + value: React.PropTypes.number, + requestChange: React.PropTypes.func, + }).isRequired, + selectedItemStyle: React.PropTypes.object, + }, + + childContextTypes: { + muiTheme: React.PropTypes.object, + }, + + getChildContext () { + return { + muiTheme: this.state.muiTheme, + }; + }, + + componentWillReceiveProps (nextProps, nextContext) { + let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme; + this.setState({muiTheme: newMuiTheme}); + }, + + getInitialState() { + return { + muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme), + }; + }, + + getValueLink: function(props) { + return props.valueLink || { + value: props.value, + requestChange: props.onChange, + }; + }, + + render(){ + const { children, selectedItemStyle } = this.props + let listItems; + let keyIndex = 0; + let styles = {}; + + if (! selectedItemStyle) { + let textColor = this.state.muiTheme.rawTheme.palette.textColor; + let selectedColor = ColorManipulator.fade(textColor, 0.2); + styles = { + backgroundColor: selectedColor, + }; + } + + listItems = React.Children.map(children, (child) => { + if (child.type.displayName === "ListItem") { + let selected = this._isChildSelected(child, this.props); + let selectedChildrenStyles = {}; + if (selected) { + selectedChildrenStyles = this.mergeStyles(styles, selectedItemStyle); + } + + let mergedChildrenStyles = this.mergeStyles( + child.props.style || {}, + selectedChildrenStyles + ); + + keyIndex += 1; + + return React.cloneElement(child, { + onTouchTap: (e) => { + this._handleItemTouchTap(e, child); + if (child.props.onTouchTap) { child.props.onTouchTap(e) }; + }, + key: keyIndex, + style: mergedChildrenStyles, + }); + } + else { + return child; + } + }); + let newChildren = listItems; + + return ( + + ); + }, + + _isChildSelected(child, props) { + let itemValue = this.getValueLink(props).value; + let childValue = child.props.value; + + return (itemValue && itemValue === childValue); + }, + + _handleItemTouchTap(e, item) { + let valueLink = this.getValueLink(this.props); + let itemValue = item.props.value; + let menuValue = valueLink.value + if ( itemValue !== menuValue) { + valueLink.requestChange(e, itemValue); + } + }, + + }); + return( composed ); +}; + diff --git a/src/index.js b/src/index.js index 3af30050944d5e..49ea3b2f2b9aa4 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,7 @@ module.exports = { RefreshIndicator: require('./refresh-indicator'), Ripples: require('./ripples/'), SelectField: require('./select-field'), + SelectableContainerEnhance: require('./hoc/selectable-enhance'), Slider: require('./slider'), SvgIcon: require('./svg-icon'), Icons: { From f76b83b04c26ec49df88e9751b7fe61775ba53dd Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Sun, 15 Nov 2015 16:32:31 +0100 Subject: [PATCH 2/2] [List] Add documentation for HOC selectable-enhance --- docs/package.json | 2 - docs/src/app/components/markdown-element.jsx | 41 ---- .../app/components/pages/components/lists.jsx | 177 +++++++++++++++--- .../app/components/raw-code/lists-code.txt | 23 ++- docs/webpack-dev-server.config.js | 4 - docs/webpack-production.config.js | 4 - src/hoc/selectable-enhance.js | 7 +- 7 files changed, 173 insertions(+), 85 deletions(-) delete mode 100644 docs/src/app/components/markdown-element.jsx diff --git a/docs/package.json b/docs/package.json index 9ab20265d8eead..da50229a91f868 100644 --- a/docs/package.json +++ b/docs/package.json @@ -15,9 +15,7 @@ }, "dependencies": { "codemirror": "^5.5.0", - "github-markdown-css": "^2.1.0", "history": "^1.11.1", - "marked": "^0.3.5", "react-addons-perf": "^0.14.0", "react-dom": "^0.14.0", "react-motion": "^0.3.1", diff --git a/docs/src/app/components/markdown-element.jsx b/docs/src/app/components/markdown-element.jsx deleted file mode 100644 index e9b689547ceff3..00000000000000 --- a/docs/src/app/components/markdown-element.jsx +++ /dev/null @@ -1,41 +0,0 @@ -const React = require('react'); -let { Paper } = require('material-ui'); -import marked from 'marked'; - -const MarkdownElement = React.createClass({ - propTypes: { - text: React.PropTypes.string.isRequired - }, - getDefaultProps() { - return { - text: '' - }; - }, - - - componentWillMount() { - marked.setOptions({ - gfm: true, - tables: true, - breaks: false, - pedantic: false, - sanitize: true, - smartLists: true, - smartypants: false - }); - }, - - render() { - const { text } = this.props, - html = marked(text || ''); - - return ( - -
-
-
- ); - }, -}); - -module.exports = MarkdownElement; diff --git a/docs/src/app/components/pages/components/lists.jsx b/docs/src/app/components/pages/components/lists.jsx index aa42c1c74eab3b..a678f5be7a8461 100644 --- a/docs/src/app/components/pages/components/lists.jsx +++ b/docs/src/app/components/pages/components/lists.jsx @@ -14,6 +14,7 @@ const ContentSend = require('svg-icons/content/send'); const EditorInsertChart = require('svg-icons/editor/insert-chart'); const FileFolder = require('svg-icons/file/folder'); const MoreVertIcon = require('svg-icons/navigation/more-vert'); +import { SelectableContainerEnhance } from 'material-ui/hoc/selectable-enhance'; const { Avatar, @@ -34,18 +35,66 @@ const { Colors } = Styles; const Code = require('lists-code'); const CodeExample = require('../../code-example/code-example'); const CodeBlock = require('../../code-example/code-block'); +let SelectableList = SelectableContainerEnhance(List); + +const Typography = Styles.Typography; +let styles = { + headline: { + fontSize: '24px', + lineHeight: '32px', + paddingTop: '16px', + marginBottom: '12px', + letterSpacing: '0', + fontWeight: Typography.fontWeightNormal, + color: Typography.textDarkBlack, + }, + subheadline: { + fontSize: '18px', + lineHeight: '27px', + paddingTop: '12px', + marginBottom: '9px', + letterSpacing: '0', + fontWeight: Typography.fontWeightNormal, + color: Typography.textDarkBlack, + }, + codeblock: { + padding: '24px', + marginBottom: '32px', + }, +} + +function wrapState(ComposedComponent) { + const StateWrapper = React.createClass({ + getInitialState() { + return { selectedIndex: 1 }; + }, + handleUpdateSelectedIndex(e, index) { + this.setState({ + selectedIndex: index, + }); + }, + render() { + return ; + }, + }); + return StateWrapper; +} + +SelectableList = wrapState(SelectableList); + export default class ListsPage extends React.Component { constructor(props) { super(props); this.state = { selectedIndex: 1 } - } - handleUpdateSelectedIndex = (index) => { - this.setState({ - selectedIndex: index, - }); + this.handleUpdateSelectedIndex = (e, index) => { + this.setState({ + selectedIndex: index, + }); + } } render() { @@ -60,6 +109,12 @@ export default class ListsPage extends React.Component { header: 'default: false', desc: 'If true, the subheader will be indented by 72px.', }, + { + name: 'selectedItemStyle', + type: 'object', + header: 'optional, only available if HOC SelectableContainerEnhance is used', + desc: 'Override the choosen inline-styles to indicate a is highlighted. You can set e.g. the background color here like this way: {{backgroundColor: #da4e49}}.', + }, { name: 'style', type: 'object', @@ -79,9 +134,9 @@ export default class ListsPage extends React.Component { desc: 'The style object to override subheader styles.', }, { - name: 'selectedLink', + name: 'valueLink', type: 'valueLink', - header: 'optional', + header: 'optional, only available if HOC SelectableContainerEnhance is used', desc: 'Makes List controllable. Highlights the ListItem whose index prop matches this "selectedLink.value". ' + '"selectedLink.requestChange" represents a callback function to change that value (e.g. in state).', }, @@ -108,13 +163,6 @@ export default class ListsPage extends React.Component { header: 'default: false', desc: 'If true, the children will be indented by 72px. Only needed if there is no left avatar or left icon.', }, - { - name: 'index', - type: 'number', - header: 'optional', - desc: 'If selectedLink prop is passed to List component, this index prop is also required. It assigns a number ' + - 'to the tab so that it can be hightlighted by the List.', - }, { name: 'leftAvatar', type: 'element', @@ -203,6 +251,13 @@ export default class ListsPage extends React.Component { header: 'optional', desc: 'Override the inline-styles of the list item\'s root element.', }, + { + name: 'value', + type: 'number', + header: 'optional, only available if HOC SelectableContainerEnhance is used', + desc: 'If valueLink prop is passed to List component, this prop is also required. It assigns an identifier ' + + 'to the listItem so that it can be hightlighted by the List.', + }, ], }, { @@ -688,30 +743,104 @@ export default class ListsPage extends React.Component { - + + } /> - } /> - } /> - } /> - } /> - + + + +
+

Selectable Lists

+

+ Basically three steps are needed: +

+
    +
  • enhance <List> with HOC
  • +
  • decide where to put state
  • +
  • implement and set valueLink
  • +
+ + +

Enhance List

+

+ Wrapping the <List> component with the higher order component "SelectableEnhance" enables + the clicked <ListItem> to be highlighted. +

+
+ + {`import { SelectableContainerEnhance } from 'material-ui/lib/hoc/selectable-enhance'; +. +. +. +var SelectableList = SelectableContainerEnhance(List); +`} + +
+ + +

Where to put state

+

+ If this component is used in conjunction with flux or redux this is a no-brainer. The callback-handler + just has to update the store. Otherwise the state can be held within e.g the parent, but it is to be to + considered that each time a <ListItem> is clicked, the state will update and the parent - including it's + children - will rerender. +

+

+ A possible solution for this is to use another hoc. An example can be found in the sourcecode + of docs/src/app/components/pages/components/lists.jsx. +

+

The valueLink

+

+ The prop 'valueLink' of <List> has to be set, to make the highlighting controllable: +

+
+ +{`valueLink={{ + value: this.state.selectedIndex, + requestChange: this.handleUpdateSelectedIndex}} +`} + +
+ A sample implementation might look like this. +
+ +{`getInitialState() { + return { selectedIndex: 1 }; +}, +handleUpdateSelectedIndex(e,index) { + this.setState({ + selectedIndex: index, +}); +`} + +
+

Adjust the <ListItem>

+

+ The prop "value" on each ListItem has to be set. This makes the item addressable for the callback. +

+
+
); - } } +} diff --git a/docs/src/app/components/raw-code/lists-code.txt b/docs/src/app/components/raw-code/lists-code.txt index 1935c3ad50fbf0..2b36dc58f771de 100644 --- a/docs/src/app/components/raw-code/lists-code.txt +++ b/docs/src/app/components/raw-code/lists-code.txt @@ -98,11 +98,20 @@ /> -// List with selected indicator - - - - - + }/> + } /> + } /> + diff --git a/docs/webpack-dev-server.config.js b/docs/webpack-dev-server.config.js index 050a66ec10931e..82baf67d318ad4 100644 --- a/docs/webpack-dev-server.config.js +++ b/docs/webpack-dev-server.config.js @@ -84,10 +84,6 @@ var config = { include: [__dirname, path.resolve(__dirname, '../src')], //include these files exclude: [nodeModulesPath] //exclude node_modules so that they are not all compiled }, - { - test: /\.css$/, // Only .css files - loader: 'style!css' // Run both loaders - } ] }, eslint: { diff --git a/docs/webpack-production.config.js b/docs/webpack-production.config.js index dcb31e9cfe18f2..e742dec2f3c6de 100644 --- a/docs/webpack-production.config.js +++ b/docs/webpack-production.config.js @@ -85,10 +85,6 @@ var config = { loader: 'raw-loader', include: path.resolve(__dirname, 'src/app/components/raw-code') }, - { - test: /\.css$/, // Only .css files - loader: 'style!css' // Run both loaders - }, ] }, eslint: { diff --git a/src/hoc/selectable-enhance.js b/src/hoc/selectable-enhance.js index ae799264bacd28..4fee1563a28acc 100644 --- a/src/hoc/selectable-enhance.js +++ b/src/hoc/selectable-enhance.js @@ -1,11 +1,10 @@ -/*eslint-disable */ const React = require('react'); const ThemeManager = require('../styles/theme-manager'); const StylePropable = require('../mixins/style-propable'); const ColorManipulator = require('../utils/color-manipulator'); const DefaultRawTheme = require('../styles/raw-themes/light-raw-theme'); -export var SelectableContainerEnhance = (Component) => { // eslint-disable-line no-var +export const SelectableContainerEnhance = (Component) => { let composed = React.createClass({ mixins: [StylePropable], @@ -97,7 +96,9 @@ export var SelectableContainerEnhance = (Component) => { // eslint-disable-line let newChildren = listItems; return ( - + + {newChildren} + ); },