Merge pull request #444 from react-toolbox/render-on-demand

Render on demand
old
Javi Velasco 2016-04-10 18:41:23 +02:00
commit 0d4015d17e
23 changed files with 234 additions and 169 deletions

View File

@ -164,7 +164,7 @@
"no-with": [2],
"one-var": [0],
"operator-assignment": [0, "always"],
"operator-linebreak": [2, "after"],
"operator-linebreak": [2, "before"],
"padded-blocks": [0],
"prefer-const": [2],
"prefer-spread": [2],

View File

@ -39,25 +39,7 @@ The previous code creates a React button component based on React Toolbox button
React Toolbox assumes that you are importing [Roboto Font](https://www.google.com/fonts/specimen/Roboto) and [Material Design Icons](https://www.google.com/design/icons/).
In order to import the fonts for you, we'd need to include them in the CSS which is considered a bad practice. If you are not including them in your app, go to the linked sites and follow the instructions.
## App component
There are some components in React Toolbox that require special positioning. For example, `Dialog` and `Drawer` components block the scroll showing a fixed positioned overlay. To handle these cases, React Toolbox needs some styling in your root node. This can be achieved by wrapping your app with a non intrusive `App` wrapper component:
```jsx
import React from 'react';
import ReactDOM from 'react-dom';
import ToolboxApp from 'react-toolbox/lib/app';
import App from './my-app';
ReactDOM.render(
<ToolboxApp>
<App />
</ToolboxApp>
, document.getElementById('app'));
```
In order to import the fonts for you, we'd need to include them in the CSS which is considered a bad practice. If you are not including them in your app, go to the linked sites and follow the instructions.
## Customization
@ -67,7 +49,7 @@ Since React Toolbox styles are written in CSS, it's pretty easy to customize you
Thanks to the power of SASS, all components in React Toolbox are configured from a variables file. The best way to customize your build is to create a custom configuration SASS file overriding configuration variables like colors or sizes.
With [toolbox-loader](https://github.com/react-toolbox/toolbox-loader) you can tell webpack where your configuration file is and it will prepend your config to each SASS build. This will result in your customized CSS for React Toolbox Components. For now you can browse the configuration files and override what you want.
With [toolbox-loader](https://github.com/react-toolbox/toolbox-loader) you can tell webpack where your configuration file is and it will prepend your config to each SASS build. This will result in your customized CSS for React Toolbox Components. For now you can browse the configuration files and override what you want.
### Via `className` property
@ -106,7 +88,7 @@ To start the documentation site locally, you'll need to install the dependencies
git clone https://github.com/react-toolbox/react-toolbox.git
npm install
cd docs/
npm install
npm install
npm start
```

View File

@ -32,11 +32,11 @@ $shadow-key-penumbra-opacity: 0.14 !default;
$shadow-ambient-shadow-opacity: 0.12 !default;
//-- Depth Shadows
$zdepth-shadow-1: 0 1px 6px rgba(0,0,0,0.12), 0 1px 4px rgba(0,0,0,0.24);
$zdepth-shadow-2: 0 3px 10px rgba(0,0,0,0.16), 0 3px 10px rgba(0,0,0,0.23);
$zdepth-shadow-3: 0 10px 30px rgba(0,0,0,0.19), 0 6px 10px rgba(0,0,0,0.23);
$zdepth-shadow-4: 0 14px 45px rgba(0,0,0,0.25), 0 10px 18px rgba(0,0,0,0.22);
$zdepth-shadow-5: 0 19px 60px rgba(0,0,0,0.30), 0 15px 20px rgba(0,0,0,0.22);
$zdepth-shadow-1: 0 1px 6px rgba(0,0,0,.12), 0 1px 4px rgba(0,0,0,.24);
$zdepth-shadow-2: 0 3px 10px rgba(0,0,0,.16), 0 3px 10px rgba(0,0,0,.23);
$zdepth-shadow-3: 0 10px 30px rgba(0,0,0,.19), 0 6px 10px rgba(0,0,0,.23);
$zdepth-shadow-4: 0 14px 45px rgba(0,0,0,.25), 0 10px 18px rgba(0,0,0,.22);
$zdepth-shadow-5: 0 19px 60px rgba(0,0,0,.3), 0 15px 20px rgba(0,0,0,.22);
//-- Animation
$animation-duration: .35s;

View File

@ -1,19 +0,0 @@
import React from 'react';
import style from './style';
const App = ({className, children}) => (
<div data-react-toolbox='app' className={`${style.root} ${className}`}>
{children}
</div>
);
App.propTypes = {
children: React.PropTypes.node,
className: React.PropTypes.string
};
App.defaultProps = {
className: ''
};
export default App;

View File

@ -1 +0,0 @@
export default from './App';

View File

@ -1,8 +0,0 @@
.root {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overflow-y: auto;
}

View File

@ -113,8 +113,8 @@ class Autocomplete extends React.Component {
const query = this.state.query.toLowerCase().trim() || '';
const values = this.values();
for (const [key, value] of this.source()) {
if (value.toLowerCase().trim().startsWith(query) &&
(!values.has(key) || !this.props.multiple)) {
if (value.toLowerCase().trim().startsWith(query)
&& (!values.has(key) || !this.props.multiple)) {
suggest.set(key, value);
}
}

View File

@ -33,10 +33,9 @@ class Calendar extends React.Component {
}
scrollToActive () {
this.refs.years.scrollTop =
this.refs.activeYear.offsetTop -
this.refs.years.offsetHeight / 2 +
this.refs.activeYear.offsetHeight / 2;
this.refs.years.scrollTop = this.refs.activeYear.offsetTop
- this.refs.years.offsetHeight / 2
+ this.refs.activeYear.offsetHeight / 2;
}
handleDayClick = (day) => {

View File

@ -1,5 +1,6 @@
import React from 'react';
import ClassNames from 'classnames';
import ActivableRenderer from '../hoc/ActivableRenderer';
import Button from '../button';
import Overlay from '../overlay';
import style from './style';
@ -28,11 +29,11 @@ const Dialog = (props) => {
{props.title ? <h6 className={style.title}>{props.title}</h6> : null}
{props.children}
</section>
{actions ?
<nav role='navigation' className={style.navigation}>
{actions}
</nav> :
null
{actions
? <nav role='navigation' className={style.navigation}>
{actions}
</nav>
: null
}
</div>
</Overlay>
@ -59,4 +60,4 @@ Dialog.defaultProps = {
type: 'normal'
};
export default Dialog;
export default ActivableRenderer()(Dialog);

View File

@ -1,5 +1,6 @@
import React from 'react';
import ClassNames from 'classnames';
import ActivableRenderer from '../hoc/ActivableRenderer';
import Overlay from '../overlay';
import style from './style';
@ -33,4 +34,4 @@ Drawer.defaultProps = {
type: 'left'
};
export default Drawer;
export default ActivableRenderer()(Drawer);

View File

@ -0,0 +1,51 @@
import React, { Component, PropTypes } from 'react';
const ActivableRendererFactory = (options = {delay: 500}) => (ActivableComponent) => {
return class ActivableRenderer extends Component {
static propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.any,
delay: PropTypes.number
};
static defaultProps = {
delay: options.delay
}
state = {
active: this.props.active,
rendered: this.props.active
};
componentWillReceiveProps (nextProps) {
if (nextProps.active && !this.props.active) this.renderAndActivate();
if (!nextProps.active && this.props.active) this.deactivateAndUnrender();
}
renderAndActivate () {
if (this.unrenderTimeout) clearTimeout(this.unrenderTimeout);
this.setState({ rendered: true, active: false }, () => {
setTimeout(() => this.setState({ active: true }), 20);
});
}
deactivateAndUnrender () {
this.setState({ rendered: true, active: false }, () => {
this.unrenderTimeout = setTimeout(() => {
this.setState({ rendered: false });
this.unrenderTimeout = null;
}, this.props.delay);
});
}
render () {
const { delay, ...others } = this.props; // eslint-disable-line no-unused-vars
const { active, rendered } = this.state;
return rendered
? <ActivableComponent {...others} active={active} />
: null;
}
};
};
export default ActivableRendererFactory;

108
components/hoc/Portal.js Normal file
View File

@ -0,0 +1,108 @@
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
class Portal extends Component {
static propTypes = {
children: PropTypes.any,
container: PropTypes.any,
lockBody: PropTypes.bool
}
static defaultProps = {
lockBody: true
}
componentDidMount () {
this._renderOverlay();
}
componentWillReceiveProps (nextProps) {
if (this._overlayTarget && nextProps.container !== this.props.container) {
this._portalContainerNode.removeChild(this._overlayTarget);
this._portalContainerNode = getContainer(nextProps.container);
this._portalContainerNode.appendChild(this._overlayTarget);
}
}
componentDidUpdate () {
this._renderOverlay();
}
componentWillUnmount () {
this._unrenderOverlay();
this._unmountOverlayTarget();
}
_mountOverlayTarget () {
if (!this._overlayTarget) {
this._overlayTarget = document.createElement('div');
this._portalContainerNode = getContainer(this.props.container);
this._portalContainerNode.appendChild(this._overlayTarget);
}
}
_unmountOverlayTarget () {
if (this._overlayTarget) {
this._portalContainerNode.removeChild(this._overlayTarget);
this._overlayTarget = null;
}
this._portalContainerNode = null;
}
_renderOverlay () {
const overlay = !this.props.children
? null
: React.Children.only(this.props.children);
if (overlay !== null) {
if (this.props.lockBody) document.body.style.overflow = 'hidden';
this._mountOverlayTarget();
this._overlayInstance = ReactDOM.unstable_renderSubtreeIntoContainer(
this, overlay, this._overlayTarget
);
} else {
this._unrenderOverlay();
this._unmountOverlayTarget();
}
}
_unrenderOverlay () {
if (this._overlayTarget) {
if (this.props.lockBody) document.body.style.overflow = 'scroll';
ReactDOM.unmountComponentAtNode(this._overlayTarget);
this._overlayInstance = null;
}
}
getMountNode () {
return this._overlayTarget;
}
getOverlayDOMNode () {
if (!this.isMounted()) {
throw new Error('getOverlayDOMNode(): A component must be mounted to have a DOM node.');
}
if (this._overlayInstance) {
if (this._overlayInstance.getWrappedDOMNode) {
return this._overlayInstance.getWrappedDOMNode();
} else {
return ReactDOM.findDOMNode(this._overlayInstance);
}
}
return null;
}
render () {
return null;
}
}
function getContainer (container) {
const _container = typeof container === 'function' ? container() : container;
return ReactDOM.findDOMNode(_container) || document.body;
}
export default Portal;

View File

@ -83,11 +83,12 @@ class Input extends React.Component {
{InputElement}
{icon ? <FontIcon className={style.icon} value={icon} /> : null}
<span className={style.bar}></span>
{labelText ?
<label className={labelClassName}>
{labelText}
{required ? <span className={style.required}> * </span> : null}
</label> : null}
{labelText
? <label className={labelClassName}>
{labelText}
{required ? <span className={style.required}> * </span> : null}
</label>
: null}
{hint ? <span className={style.hint}>{hint}</span> : null}
{error ? <span className={style.error}>{error}</span> : null}
{maxLength ? <span className={style.counter}>{length}/{maxLength}</span> : null}

View File

@ -1,5 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Portal from '../hoc/Portal';
import ClassNames from 'classnames';
import style from './style';
@ -18,26 +18,18 @@ class Overlay extends React.Component {
};
componentDidMount () {
this.app = document.querySelector('[data-react-toolbox="app"]') || document.body;
this.node = document.createElement('div');
this.node.setAttribute('data-react-toolbox', 'overlay');
this.app.appendChild(this.node);
this.handleRender();
if (this.props.active) {
this.escKeyListener = document.body.addEventListener('keydown', this.handleEscKey.bind(this));
}
}
componentDidUpdate () {
this.handleRender();
if (this.props.active && !this.escKeyListener) {
this.escKeyListener = document.body.addEventListener('keydown', this.handleEscKey.bind(this));
}
}
componentWillUnmount () {
ReactDOM.unmountComponentAtNode(this.node);
this.app.removeChild(this.node);
if (this.escKeyListener) {
document.body.removeEventListener('keydown', this.handleEscKey);
this.escKeyListener = null;
@ -50,24 +42,20 @@ class Overlay extends React.Component {
}
}
handleRender () {
render () {
const className = ClassNames(style.root, {
[style.active]: this.props.active,
[style.invisible]: this.props.invisible
}, this.props.className);
const overlay = (
<div className={className}>
<div className={style.overlay} onClick={this.props.onClick} />
{this.props.children}
</div>
return (
<Portal>
<div className={className}>
<div className={style.overlay} onClick={this.props.onClick} />
{this.props.children}
</div>
</Portal>
);
ReactDOM.unstable_renderSubtreeIntoContainer(this, overlay, this.node);
}
render () {
return React.DOM.noscript();
}
}

View File

@ -1,5 +1,6 @@
import React from 'react';
import ClassNames from 'classnames';
import ActivableRenderer from '../hoc/ActivableRenderer';
import Button from '../button';
import FontIcon from '../font_icon';
import Overlay from '../overlay';
@ -21,24 +22,13 @@ class Snackbar extends React.Component {
type: React.PropTypes.string
};
state = {
curTimeout: null
};
componentWillReceiveProps (nextProps) {
if (nextProps.active && nextProps.timeout) {
if (this.state.curTimeout) clearTimeout(this.state.curTimeout);
const curTimeout = setTimeout(() => {
if (this.curTimeout) clearTimeout(this.curTimeout);
this.curTimeout = setTimeout(() => {
nextProps.onTimeout();
this.setState({
curTimeout: null
});
this.curTimeout = null;
}, nextProps.timeout);
this.setState({
curTimeout
});
}
}
@ -60,4 +50,4 @@ class Snackbar extends React.Component {
}
}
export default Snackbar;
export default ActivableRenderer()(Snackbar);

View File

@ -21,13 +21,11 @@ class Tabs extends React.Component {
};
componentDidMount () {
!this.props.disableAnimatedBottomBorder &&
this.updatePointer(this.props.index);
!this.props.disableAnimatedBottomBorder && this.updatePointer(this.props.index);
}
componentWillReceiveProps (nextProps) {
!this.props.disableAnimatedBottomBorder &&
this.updatePointer(nextProps.index);
!this.props.disableAnimatedBottomBorder && this.updatePointer(nextProps.index);
}
componentWillUnmount () {

View File

@ -54,7 +54,7 @@ class Clock extends React.Component {
handleCalculateShape = () => {
const { top, left, width } = this.refs.placeholder.getBoundingClientRect();
this.setState({
center: { x: left + width / 2, y: top + width / 2 },
center: { x: left + width / 2 - window.scrollX, y: top + width / 2 - window.scrollX },
radius: width / 2
});
};

View File

@ -26,8 +26,8 @@ class TimePickerDialog extends React.Component {
displayTime: this.props.value
};
componentWillUpdate (nextProps) {
if (!this.props.active && nextProps.active) {
componentDidUpdate (prevProps) {
if (!prevProps.active && this.props.active) {
setTimeout(this.refs.clock.handleCalculateShape, 1000);
}
}

View File

@ -1,23 +1,21 @@
export default {
getMousePosition (event) {
return {
x: event.pageX,
y: event.pageY
x: event.pageX - window.scrollX,
y: event.pageY - window.scrollY
};
},
getTouchPosition (event) {
return {
x: event.touches[0].pageX,
y: event.touches[0].pageY
x: event.touches[0].pageX - window.scrollX,
y: event.touches[0].pageY - window.scrollY
};
},
pauseEvent (event) {
event.stopPropagation();
event.preventDefault();
event.returnValue = false;
event.cancelBubble = true;
},
addEventsToDocument (eventMap) {

View File

@ -35,24 +35,6 @@ React Toolbox assumes that you are importing [Roboto Font](https://www.google.co
In order to import the fonts for you, we'd need to include them in the CSS which is considered a bad practice. If you are not including them in your app to the linked sites and follow the instructions.
## App component
There are some components in React Toolbox that requires special positioning. For example, `Dialog` and `Drawer` components block the scroll showing a fixed positioned overlay. To handle these cases, React Toolbox needs some styling in your root node. This can be achieved wrapping your app with a nonintrusive `App` wrapper component:
```jsx
import React from 'react';
import ReactDOM from 'react-dom';
import ToolboxApp from 'react-toolbox/lib/app';
import App from './my-app';
ReactDOM.render(
<ToolboxApp>
<App />
</ToolboxApp>
, document.getElementById('app'));
```
## Customization
Since React Toolbox styles are written in CSS it's pretty easy to customize your components. We have several ways:

View File

@ -1,9 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import { Router, Route, browserHistory } from 'react-router';
import 'react-toolbox/commons';
import { App } from 'react-toolbox';
import Home from './components/layout/home';
import Install from './components/layout/install';
@ -11,13 +10,10 @@ import Main from './components/layout/main';
ReactDOM.render((
<Router history={browserHistory}>
<Route component={App}>
<Route path="/" component={Home} />
<Route path="/install" component={Install} />
<Route path="/components" component={Main}>
<Route path=":component" />
</Route>
<IndexRoute component={Home}/>
<Route path="/" component={Home} />
<Route path="/install" component={Install} />
<Route path="/components" component={Main}>
<Route path=":component" />
</Route>
</Router>
), document.getElementById('app'));

View File

@ -1,6 +1,5 @@
/* global VERSION */
import React from 'react';
import App from '../components/app';
import AppBarToolbox from '../components/app_bar';
import Avatar from './components/avatar';
import ButtonToolbox from '../components/button';
@ -32,7 +31,7 @@ const _hrefProject = () => {
};
const Root = () => (
<App className={style.app}>
<div className={style.app}>
<AppBarToolbox fixed flat className={style.appbar}>
<h1>React Toolbox <small>Spec {VERSION}</small></h1>
<ButtonToolbox
@ -66,7 +65,7 @@ const Root = () => (
<Table />
<Tabs />
<Tooltip />
</App>
</div>
);
export default Root;

View File

@ -11,11 +11,11 @@ function generateDesciption (description) {
function generatePropType (type) {
let values;
if (Array.isArray(type.value)) {
values = '(`' +
type.value.map(function (typeValue) {
values = '(`'
+ type.value.map(function (typeValue) {
return typeValue.name || typeValue.value;
}).join('`,`') +
'`)';
}).join('`,`')
+ '`)';
} else {
values = type.value;
}
@ -33,11 +33,11 @@ function generateProp (propName, prop) {
}
return (
`| \`${propName}\` ${prop.required ? '(required)' : ''}` +
`| ${(prop.type ? generatePropType(prop.type) : '')} ` +
`| ${(prop.defaultValue ? `\`${prop.defaultValue}\`` : '')} ` +
`| ${(prop.description ? prop.description : '')} ` +
'|'
`| \`${propName}\` ${prop.required ? '(required)' : ''}`
+ `| ${(prop.type ? generatePropType(prop.type) : '')} `
+ `| ${(prop.defaultValue ? `\`${prop.defaultValue}\`` : '')} `
+ `| ${(prop.description ? prop.description : '')} `
+ '|'
);
}
@ -45,20 +45,19 @@ function generateProps (props) {
const title = '### Properties';
return (
`${title}\n` +
'| Name | Type | Default | Description |\n' +
'|:-----|:-----|:-----|:-----|\n' +
Object.keys(props).sort().map(propName => {
`${title}\n`
+ '| Name | Type | Default | Description |\n'
+ '|:-----|:-----|:-----|:-----|\n'
+ Object.keys(props).sort().map(propName => {
return generateProp(propName, props[propName]);
}).join('\n')
);
}
function generateMarkdown (name, reactAPI) {
const markdownString =
generateTitle(name) + '\n' +
(reactAPI.description ? generateDesciption(reactAPI.description) + '\n' : '\n') +
generateProps(reactAPI.props);
const markdownString = generateTitle(name) + '\n'
+ (reactAPI.description ? generateDesciption(reactAPI.description) + '\n' : '\n')
+ generateProps(reactAPI.props);
return markdownString;
}