├── .babelrc ├── .npmignore ├── CHANGELOG.md ├── README.md ├── build.sh ├── demo ├── dist │ ├── demo.html │ ├── index.js │ └── index.js.map ├── fixtures │ ├── questions.json │ └── user.json ├── package.json └── src │ ├── actions │ └── actions.js │ ├── components │ ├── Answer.js │ ├── App.js │ ├── Byline.js │ ├── Keywords.js │ ├── Question.js │ ├── QuestionDetail.js │ ├── QuestionList.js │ ├── SubmitAnswer.js │ └── SubmitQuestion.js │ ├── index.js │ ├── reducers │ └── reducers.js │ └── styles │ └── styles.js ├── dist ├── index.js └── index.js.map ├── gulpfile.js ├── package.json ├── scripts └── make-package-json.js ├── src ├── components │ ├── Button.js │ ├── ComponentList.js │ ├── Console.js │ ├── Controls.js │ ├── DataIconCell.js │ ├── LifeCycle.js │ ├── Method.js │ ├── PopoutWindow.js │ └── Utf8Char.js ├── index.js ├── root.js ├── settingsManager.js ├── store │ ├── actions.js │ ├── lifecycleConfig.js │ └── reducers.js ├── styles │ └── styles.js └── vendor │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── bootstrapStyles.js │ ├── bootstrapTableStyles.js │ └── react-bootstrap-table-all.min.css ├── test ├── ConfigureSpec.js ├── MonitorSpec.js └── fixtures │ └── entries.json └── webpack.config-test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "add-module-exports" 9 | ] 10 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gitignore 3 | .idea/ 4 | *.tgz 5 | .babelrc 6 | build.sh 7 | gulpfile.js 8 | npm-debug.log 9 | node_modules/ 10 | demo 11 | src 12 | test 13 | scripts 14 | webpack.config-test.js -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v0.0.8 (2016-10-18) 2 | 3 | #### New Features 4 | 5 | - You can disable Continuous Refresh of the Monitor and manually refresh instead for better performance 6 | - Monitor shows whether setState has been called in a component 7 | - Better styling in Monitor for showing presence of overridden lifecycle methods in wrapped component 8 | - The Component List is sortable by column 9 | - You can filter the Component List 10 | - Component List is responsive 11 | - Faster performance 12 | 13 | #### Bug Fixes 14 | 15 | - Presence of overridden lifecycle methods in wrapped component now correctly detected 16 | - States/props now correctly flagged as different if one is null 17 | - Component names now displayed properly in React debugger 18 | - Scrollbar now works correctly in Component List 19 | 20 | 21 | ### v0.0.5 (2016-10-10) 22 | 23 | #### New Features 24 | 25 | - Global configuration can be set from webpack build 26 | - Component-level configuration can be set through arguments passed to Visible wrapper function -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visible React 2 | 3 | See a visual representation of the component lifecycle events as they occur in your React application. Analyze and correct performance lags caused by unnecessary component rerenders. 4 | 5 | [Try out the demo](https://rawgit.com/rkendall/visible-react/master/demo/dist/demo.html) 6 | (Your popup blocker must be disabled) 7 | 8 | [This tool has not yet been tested on versions of React prior to 15 or on browsers other than Chrome and Firefox.] 9 | 10 | ## What Is Visible React? 11 | 12 | Include **Visible React** in your React project, and a Monitor Window will open when your project launches in a development environment. (You must disable your browser's popup blocker to enable the window.) The Monitor provides a visualization of the lifecycle events in every component mounted in your app, along with views of the data passing through them. It also warns you about components that are rerendering unnecessarily, potentially slowing performance. In a production environment, it optionally provides a means for avoiding these unnecessary rerenders. 13 | 14 | ## Why Visible React? 15 | 16 | #### Learning 17 | 18 | Creating full-featured, efficient components in React requires a thorough understanding of the component rendering lifecycle and the methods associated with lifecycle events. The React lifecycle can be confusing even to experienced React users, however, because of its complexity. There are different events for the initial render and subsequent rerenders. You have to know which of the available versions of props and state to read at any given point in the lifecycle. You can set the component's state inside some lifecycle methods but not others, and optimal performance depends upon setting the state in the correct places. Performance further depends upon preventing unnecessary rerenders by checking for changes in data. **Visible React** clarifies these issues by means of detailed visualizations. 19 | 20 | #### Debugging 21 | 22 | In the **Visible React** Monitor, trace the flow of props and states through the different components of your app, view and diff props/state values, see which lifecycle events have been triggered, and see whenever setState has been called. In the browser console, view a log of all the lifecycle events triggered by your components. 23 | 24 | #### Performance Analysis 25 | Your app can be slowed by unnecessary rerenders that are normally very difficult to identify. **Visible React** finds these performance bottlenecks for you. 26 | 27 | #### Performance Enhancement 28 | 29 | **Visible React** provides a global solution for preventing many unnecessary rerenders by overriding the `shouldComponentUpdate` method. 30 | 31 | ## Adding It to Your Project 32 | 33 | Install from NPM. 34 | 35 | `npm install visible-react` 36 | 37 | Import **Visible React** into each component you want to monitor. 38 | 39 | `import Visible from 'visible-react';` 40 | 41 | or 42 | 43 | `var Visible = require('visible-react');` 44 | 45 | Wrap the component's export in the `Visible` function. 46 | 47 | `export default Visible()(MyWidget);` 48 | 49 | If the component has other wrappers, wrap it with Visible first. 50 | 51 | `export default Radium(Visible()(MyWidget));` 52 | 53 | 54 | ## The Visible React Monitor 55 | 56 | The Component Pane on the left side of the Monitor presents a list of all the components in your app that are currently or were formerly mounted. Colored dots appear in the leftmost column if the component's props or state changed during the last render (blue dots for props, green for state). The **Rendered** column shows how many times the component has been rendered since mounting. (The value is reset upon each unmounting.) The **Warnings** column shows the number of times that **Visible React** has prevented a component from unnecessarily rerendering when its props and state have not changed. To filter the component list, type filtering text into the **Filter** field. To sort the list, click on the header of the column you wish to sort by. 57 | 58 | The Lifecycle Pane on the right side of the monitor provides a visualization of the selected component's lifecycle events. The three columns of cards represent the three possible types of lifecycle activity: initial mounting, updating, and unmounting. Each card represents a lifecycle method, and a glow effect shows which methods were called as part of the component's last lifecycle activity. Each Method Card provides the following information. 59 | 60 | * A yellow box around the method name indicates that the method is called by the underlying wrapped component. 61 | * The **Times called** field shows the number of times the method has been called since mounting. 62 | * The card lists all the props and state variables available in the method and shows the values they had the last time that method was called. Click on a value to see its complete JSON. 63 | * If a new props or state value is different from the old one (eg., `nextProps` compared to `this.props`), both values are shown. If they are the same, only a single value is shown for both variables. The popup JSON view shows a line-by-line diff of any differences between old and new values. 64 | * Colored text (blue for props, green for state) shows where in the lifecycle any data changes have been introduced. 65 | * A gray box indicates whether the state can be set within the method. 66 | * A green box around `setState` indicates that `setState` was called by the method. 67 | * At the bottom of the **shouldComponentUpdate** card a warning will appear if **Visible React** has prevented a component from unnecessarily rerendering when its props and state have not changed. 68 | * At the bottom of the **componentDidMount** and **componentDidUpdate** cards, a warning will appear if `setState` was called within these methods, indicating that the extra rerender this causes may have been unnecessary. 69 | 70 | If your app is complex or uses large data sets, the **Visible React** Monitor Window may slow its performance. If this is the case, you can eliminate all or most of this performance lag by unselecting the Continuous Refresh checkbox at the left. Then click the Refresh button every time you want to refresh the contents of the Monitor Window. 71 | 72 | ## Managing Rerenders with Life Insurance 73 | 74 | By default, React rerenders a component every time the props or state are updated, even if the actual data hasn’t changed. These unnecessary rerenders can slow performance if the component is large or has a lot of children (which will also rerender), or if multiple rerenders occur in quick succession. 75 | 76 | React provides a preventive measure for this _rerenderitis_ in the form of the `shouldComponentUpdate` method. You can use it to evaluate the component’s state and props, and if nothing has changed, you can return false from the method to prevent a rerender. There are complications, though. If the data structure is deeply nested you’ll have to do a deep comparison to avoid a false negative, unless you’re using immutable data structures. A deep compare on a very large data structure could potentially take longer than the rerender it is preventing. 77 | 78 | This is where **Visible React** comes in. It provides a feature called Life Insurance, which by default performs the following functions in a development environment. 79 | 80 | 1. Overrides `shouldComponentUpdate` in every wrapped component 81 | 1. Intercepts the returned value 82 | 1. If an existing `shouldComponentUpdate` method returns false, Life Insurance returns false 83 | 1. Otherwise it deep compares the new and old props and states 84 | 1. Returns false if there are no changes (preventing a rerender) 85 | 1. Provides warnings in the Visible React Monitor if unnecessary rerenders have been prevented 86 | 87 | Life Insurance is disabled by default in production environments, but you can enable it to prevent unnecessary rerenders. See below for instructions on configuring **Visible React** and Life Insurance. 88 | 89 | ## Configuring Visible React 90 | 91 | ### Quick Start 92 | 93 | To specify a development or production environment for **Visible React**, you must include a task in your build process to replace `process.env.NODE_ENV` in your build with the appropriate value. (This is also the recommended method for specifying the environment to React.) If no environment is specified, **Visible React** defaults to 'development.' The following example sets the environment to 'production' in a Gulp build. (Requires `gulp-replace`.) 94 | 95 | ```javascript 96 | gulp.task('set-vr', function() { 97 | gulp.src('build/bundle.js') 98 | .pipe(replace('process.env.NODE_ENV', JSON.stringify('production'))) 99 | .pipe(gulp.dest('dist')); 100 | }); 101 | ``` 102 | 103 | To activate preventive comparison in a 'production' environment, make the following addition. This will trigger a shallow comparison on the data going into each component and prevent any unnecessary rerenders. This is the same functionality as that provided by React's pureRenderMixin, except that Life Insurance doesn't override a false value that is returned from an existing `shouldComponentUpdate` method. See below for more configuration details. 104 | 105 | ```javascript 106 | gulp.task('set-vr', function() { 107 | gulp.src('build/bundle.js') 108 | .pipe(replace('process.env.NODE_ENV', JSON.stringify('production'))) 109 | .pipe(replace('process.env.VR_PROD_ENABLED', JSON.stringify('all'))) 110 | .pipe(gulp.dest('dist')); 111 | }); 112 | ``` 113 | 114 | ### Global Configuration 115 | 116 | The following variables can be set with a build tool, as shown above. Each variable name must be prefixed with either `VR_DEV_` or `VR_PROD_` to determine which environment (development or production) it will take effect in. For example, `VR_DEV_ENABLED` determines whether **Visible React** is enabled in a development environment. The first three variables listed below all accept the same three values: _'all'_ enables the feature in all components; *'none'* disables it in all components; *'selected'* enables it only where specified by a component configuration setting. (See below for setting component configuration options.) 117 | 118 | **ENABLED** 119 | *'all'|'selected'|'none'* 120 | Whether **Visible React** is enabled. If set to *'none'*, the features controlled by the following variables will all be disabled and the `Visible` function will noninvasively pass values through to the wrapped component without overriding any methods. Defaults to _'all'_ in development and _'none'_ in production. 121 | **MONITOR** 122 | *'all'|'selected'|'none'* 123 | Whether or not to display the Monitor Window. Defaults to _'all'_ in development and _'none'_ in production. 124 | **LOGGING** 125 | *'all'|'selected'|'none'* 126 | Whether to log messages to the browser console for each lifecycle event and setState call. Defaults to _'selected'_ in development and _'none'_ in production. 127 | **CONTROL** 128 | *'all'|'none'* 129 | Whether to prevent component rerenders if props or state have not changed. 'selected' is not an option for this variable. Defaults to _'all'_ in both environments. 130 | **COMPARE** 131 | *'none'|'shallow'|'deep'* 132 | The type of comparison to perform. If CONTROL is false, no comparison will be performed. Defaults to _'deep'_ in development and _'shallow'_ in production. 133 | 134 | #### Setting Variables in a Build 135 | 136 | Normally you will want to have one configuration for development that enables the dev tools and one for production that disables them. It's easy to accomplish a conditional configuration with a Gulp task, and you can set the relevant configuration options either in environment variables or directly in Gulp. The following Gulp task will configure **Visible React** using the values found in any existing Node environment variables. If any variable is undefined, **Visible React** will use its default value. The following example will set the environment to 'production,' enable logging for all components in 'development,' and enable **Visible React** in 'production.' 137 | 138 | ```javascript 139 | var replace = require('gulp-replace'); 140 | 141 | process.env.NODE_ENV = 'production'; 142 | process.env.VR_DEV_LOGGING = 'all'; 143 | process.env.VR_PROD_ENABLED = 'all'; 144 | 145 | gulp.task('set-vr', function() { 146 | 147 | gulp.src('build/bundle.js') 148 | 149 | .pipe(replace('process.env.NODE_ENV', JSON.stringify(process.env.NODE_ENV) || null)) 150 | 151 | .pipe(replace('process.env.VR_DEV_ENABLED', JSON.stringify(process.env.VR_DEV_ENABLED) || null)) 152 | .pipe(replace('process.env.VR_DEV_MONITOR', JSON.stringify(process.env.VR_DEV_MONITOR) || null)) 153 | .pipe(replace('process.env.VR_DEV_LOGGING', JSON.stringify(process.env.VR_DEV_LOGGING) || null)) 154 | .pipe(replace('process.env.VR_DEV_CONTROL', JSON.stringify(process.env.VR_DEV_CONTROL) || null)) 155 | .pipe(replace('process.env.VR_DEV_COMPARE', JSON.stringify(process.env.VR_DEV_COMPARE) || null)) 156 | 157 | .pipe(replace('process.env.VR_PROD_ENABLED', JSON.stringify(process.env.VR_PROD_ENABLED) || null)) 158 | .pipe(replace('process.env.VR_PROD_MONITOR', JSON.stringify(process.env.VR_PROD_MONITOR) || null)) 159 | .pipe(replace('process.env.VR_PROD_LOGGING', JSON.stringify(process.env.VR_PROD_LOGGING) || null)) 160 | .pipe(replace('process.env.VR_PROD_CONTROL', JSON.stringify(process.env.VR_PROD_CONTROL) || null)) 161 | .pipe(replace('process.env.VR_PROD_COMPARE', JSON.stringify(process.env.VR_PROD_COMPARE) || null)) 162 | 163 | .pipe(gulp.dest('dist')); 164 | 165 | }); 166 | ``` 167 | 168 | If you don't wish to set environment variables, you can accomplish the same configuration as above with the following more-concise syntax. 169 | 170 | ```javascript 171 | gulp.task('set-vr', function() { 172 | gulp.src('build/bundle.js') 173 | .pipe(replace('process.env.NODE_ENV', JSON.stringify('production'))) 174 | .pipe(replace('process.env.VR_DEV_LOGGING', JSON.stringify('all'))) 175 | .pipe(replace('process.env.VR_PROD_ENABLED', JSON.stringify('all')) 176 | .pipe(gulp.dest('dist')); 177 | }); 178 | ``` 179 | 180 | ### Component Configuration 181 | 182 | For more fine-grained control, you can pass arguments to the `Visible` wrapper function to control how **Visible React** handles individual components. You can configure a component by passing a configuration object as an argument to the `Visible` function. The supported variables correspond to the global configuration variables, but most of them accept Boolean values, since they apply only to a single component. All values default to false, except for `compare`, which defaults to null. 183 | 184 | **enabled** 185 | *true|false* 186 | Whether **Visible React** is enabled for the component. 187 | **monitor:** 188 | *true|false* 189 | Whether or not to include the component in the Monitor Window display. 190 | **logging:** 191 | *true|false* 192 | Whether to log messages to the browser console for the component. 193 | **compare** 194 | *'none'|'shallow'|'deep'* 195 | Type of comparison to perform for the component. This value will override any `compare` value specified in a build variable. 196 | 197 | Component configuration lets you optimize performance by specifying which type of preventive comparison to perform for each component. Shallow compares can return false negatives. Deep compares, while always accurate, are not always cost effective. Logging can be quite verbose, so it can be useful to log calls for only selected components. Enabling **Visible React** only for selected components can be useful if you find that the Monitor Window is degrading the performance of your app. Below is an example of a component configuration. 198 | 199 | ```javascript 200 | const options = { 201 | enabled: true, 202 | monitor: true, 203 | logging: true, 204 | compare: 'shallow' 205 | } 206 | export default Visible(options)(MyComponent); 207 | ``` 208 | 209 | ### More Configuration Examples 210 | 211 | Sets environment to 'development' and enables logging and monitoring only for MyComponent in that environment. 212 | 213 | ```javascript 214 | // In gulpfile.js 215 | gulp.task('set-vr', function() { 216 | gulp.src('build/bundle.js') 217 | .pipe(replace('process.env.NODE_ENV', JSON.stringify('development'))) 218 | .pipe(replace('process.env.VR_DEV_MONITOR', JSON.stringify('selected')) 219 | .pipe(replace('process.env.VR_DEV_LOGGING', JSON.stringify('selected'))) 220 | .pipe(gulp.dest('dist')); 221 | }); 222 | 223 | // In MyComponent.js 224 | const options = { 225 | monitor: true, 226 | logging: true 227 | } 228 | export default Visible(options)(MyComponent); 229 | ``` 230 | 231 | Sets environment to 'production' and enables **Visible React** in production to do a only preventive shallow compare (the default) for every component except MyComponent, which undergoes a deep compare. 232 | 233 | ```javascript 234 | // In gulpfile.js 235 | gulp.task('set-vr', function() { 236 | gulp.src('build/bundle.js') 237 | .pipe(replace('process.env.NODE_ENV', JSON.stringify('production'))) 238 | .pipe(replace('process.env.VR_PROD_ENABLED', JSON.stringify('all')) 239 | .pipe(gulp.dest('dist')); 240 | }); 241 | 242 | // In MyComponent.js 243 | const options = { 244 | compare: 'deep' 245 | } 246 | export default Visible(options)(MyComponent); 247 | ``` 248 | 249 | Sets environment to 'production' and prevents comparison in production in every component except MyComponent, which undergoes a shallow compare. 250 | 251 | ```javascript 252 | // In gulpfile.js 253 | gulp.task('set-vr', function() { 254 | gulp.src('build/bundle.js') 255 | .pipe(replace('process.env.NODE_ENV', JSON.stringify('production'))) 256 | .pipe(replace('process.env.VR_PROD_COMPARE', JSON.stringify('none'))) 257 | .pipe(gulp.dest('dist')); 258 | }); 259 | 260 | // In MyComponent.js 261 | const options = { 262 | compare: 'shallow' 263 | } 264 | export default Visible(options)(MyComponent); 265 | ``` 266 | 267 | ### License 268 | 269 | [MIT](LICENSE) 270 | Copyright (c) 2016 [Robert Kendall](http://robertkendall.com) -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm install 4 | cd demo 5 | npm install 6 | cd ../ 7 | npm run gulp -------------------------------------------------------------------------------- /demo/dist/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Questions and Answers 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/fixtures/questions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "title": "What is the quickest way to double my money?", 5 | "description": "I need expert financial advice on this matter.", 6 | "user": "Jane Doe", 7 | "date": 1450016214776, 8 | "keywords": ["money", "finance", "investment"], 9 | "answers": [ 10 | { 11 | "text": "The experts are all in agreement: Just fold it in half.", 12 | "user": "Joe Blow", 13 | "date": 1470716314876 14 | } 15 | ] 16 | }, 17 | { 18 | "id": "2", 19 | "title": "What language do they speak in Cuba?", 20 | "description": "I am planning a vacation there and want to learn some of the vocabulary in preparation.", 21 | "user": "John Smith", 22 | "date": 1470716315776, 23 | "keywords": ["language", "Cuba", "travel"], 24 | "answers": [ 25 | { 26 | "text": "Everyone knows that they speak Cubic in Cuba.", 27 | "user": "Juan", 28 | "date": 1470716316776 29 | } 30 | ] 31 | }, 32 | { 33 | "id": "3", 34 | "title": "What's black and white and red all over?", 35 | "description": "I've searched the forum and couldn't find an answer.", 36 | "user": "May Brown", 37 | "date": 1470716319776, 38 | "keywords": ["colors", "riddles"], 39 | "answers": [ 40 | { 41 | "text": "A newspaper.", 42 | "user": "Rupert Murdoch", 43 | "date": 1472742254565 44 | }, 45 | { 46 | "text": "What's a newspaper?", 47 | "user": "Mark Zuckerberg", 48 | "date": 1472742254565 49 | }, 50 | { 51 | "text": "A sunburned zebra.", 52 | "user": "May", 53 | "date": 1472771242555 54 | }, 55 | { 56 | "text": "An old picture of Lenin.", 57 | "user": "Vladimir", 58 | "date": 1472772252565 59 | } 60 | ] 61 | } 62 | 63 | ] -------------------------------------------------------------------------------- /demo/fixtures/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "me" 3 | } -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visible-react-demo", 3 | "version": "0.0.2", 4 | "description": "Demo of Visible React", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "mocha --compilers js:babel-register --recursive" 8 | }, 9 | "author": "Robert Kendall (http://robertkendall.com)", 10 | "license": "MIT", 11 | "dependencies": { 12 | "color": "^0.11.3", 13 | "deepcopy": "^0.6.3", 14 | "json-loader": "^0.5.4", 15 | "material-ui": "^0.15.3", 16 | "moment": "^2.14.1", 17 | "node-uuid": "^1.4.7", 18 | "radium": "^0.18.1", 19 | "react": "^15.3.0", 20 | "react-dom": "^15.3.0", 21 | "react-redux": "^4.4.5", 22 | "react-router": "^2.6.1", 23 | "react-tap-event-plugin": "^1.0.0", 24 | "react-time": "^4.2.0", 25 | "redux": "^3.5.2", 26 | "script-loader": "^0.7.0", 27 | "visible-react": "^0.0.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /demo/src/actions/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const addQuestion = (question) => { 4 | return { 5 | type: 'ADD_QUESTION', 6 | question 7 | }; 8 | }; 9 | 10 | export const addAnswer = (questionId, answer) => { 11 | return { 12 | type: 'ADD_ANSWER', 13 | questionId, 14 | answer 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /demo/src/components/Answer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component, PropTypes} from 'react'; 4 | import Visible from 'visible-react'; 5 | 6 | import Byline from './Byline'; 7 | import styles from '../styles/styles'; 8 | 9 | class Answer extends Component { 10 | 11 | static propTypes = { 12 | id: PropTypes.string, 13 | user: PropTypes.string.isRequired, 14 | date: PropTypes.number.isRequired, 15 | text: PropTypes.string.isRequired 16 | }; 17 | 18 | static defaultProps = { 19 | id: '' 20 | }; 21 | 22 | styles = { 23 | text: { 24 | paddingTop: '10px', 25 | borderTop: '1px solid lightgray', 26 | fontSize: '14px' 27 | } 28 | }; 29 | 30 | componentWillReceiveProps() { 31 | this.setState({ 32 | value: 'State is set here just for fun' 33 | }); 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 | 40 |
{this.props.text}
41 |
42 | ); 43 | } 44 | 45 | } 46 | 47 | export default Visible()(Answer); -------------------------------------------------------------------------------- /demo/src/components/App.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import {connect} from 'react-redux'; 5 | import {bindActionCreators} from 'redux'; 6 | import Radium from 'radium'; 7 | import Visible from 'visible-react'; 8 | 9 | import * as questionActions from '../actions/actions'; 10 | 11 | class App extends Component { 12 | 13 | styles = { 14 | app: { 15 | display: 'flex', 16 | flexDirection: 'column', 17 | alignItems: 'center', 18 | fontFamily: 'Helvetica, Arial, sans-serif' 19 | }, 20 | container: { 21 | '@media (min-width: 800px)': { 22 | width: '800px' 23 | }, 24 | '@media (max-width: 799px)': { 25 | width: '100%' 26 | } 27 | }, 28 | heading: { 29 | display: 'flex', 30 | justifyContent: 'center', 31 | alignItems: 'center', 32 | width: '100%', 33 | height: '50px', 34 | marginBottom: '20px', 35 | color: 'white', 36 | backgroundColor: '#00bcd4', 37 | fontSize: '26px', 38 | fontWeight: 'bold' 39 | } 40 | }; 41 | 42 | render() { 43 | return ( 44 |
45 |
The Big Questions
46 |
47 | {this.props.children} 48 |
49 |
50 | ); 51 | } 52 | 53 | } 54 | 55 | function mapStateToProps(state) { 56 | return { 57 | questions: state.questions 58 | }; 59 | } 60 | 61 | const visibleSettings = { 62 | enabled: true, 63 | monitor: true 64 | }; 65 | 66 | export default connect( 67 | mapStateToProps 68 | )(Radium(Visible(visibleSettings)(App))); -------------------------------------------------------------------------------- /demo/src/components/Byline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component, PropTypes} from 'react'; 4 | import Time from 'react-time' 5 | import Visible from 'visible-react'; 6 | 7 | const Byline = React.createClass({ 8 | 9 | propTypes: { 10 | user: PropTypes.string.isRequired, 11 | date: PropTypes.number.isRequired, 12 | answerCount: PropTypes.number 13 | }, 14 | 15 | styles: { 16 | container: { 17 | display: 'flex', 18 | marginBottom: '10px', 19 | fontSize: '12px' 20 | }, 21 | name: { 22 | color: '#ff4081' 23 | }, 24 | answerCount: { 25 | marginLeft: '30px', 26 | fontWeight: 'bold' 27 | } 28 | }, 29 | 30 | getDefaultProps: function() { 31 | return { 32 | answerCount: null 33 | } 34 | }, 35 | 36 | getInitialState: function() { 37 | return { 38 | user: this.props.user 39 | }; 40 | }, 41 | 42 | componentWillReceiveProps() { 43 | this.setState({ 44 | propsReceived: true 45 | }); 46 | }, 47 | 48 | componentWillMount() { 49 | console.log('Byline will mount'); 50 | }, 51 | 52 | componentWillUnmount() { 53 | console.log('Byline mounted'); 54 | }, 55 | 56 | getAnswerCount: function() { 57 | if (this.props.answerCount === null) { 58 | return false; 59 | } 60 | let label = 'answer'; 61 | if (this.props.answerCount !== 1) { 62 | label += 's'; 63 | } 64 | return ( 65 |
66 | {this.props.answerCount} {label} 67 |
68 | ) 69 | }, 70 | 71 | getName: function() { 72 | return ( 73 | 74 | {this.props.user} 75 | 76 | ) 77 | }, 78 | 79 | render: function() { 80 | return ( 81 |
82 |
83 | Posted by {this.getName()},
85 | {this.getAnswerCount()} 86 |
87 | ); 88 | } 89 | 90 | }); 91 | 92 | export default Visible()(Byline); -------------------------------------------------------------------------------- /demo/src/components/Keywords.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component, PropTypes} from 'react'; 4 | import Visible from 'visible-react'; 5 | 6 | class Keywords extends Component { 7 | 8 | static propTypes = { 9 | keywords: PropTypes.array.isRequired 10 | }; 11 | 12 | styles = { 13 | keywords: { 14 | display: 'flex', 15 | justifyContent: 'flex-begin', 16 | marginBottom: '5px', 17 | fontSize: '11px' 18 | }, 19 | keyword: { 20 | marginRight: '10px', 21 | padding: '2px 4px', 22 | border: '1px solid lightgray', 23 | backgroundColor: 'white' 24 | } 25 | }; 26 | 27 | 28 | formatKeywords = (keywords) => { 29 | return keywords.map((keyword, ind) => { 30 | return ( 31 |
32 | {keyword} 33 |
34 | ); 35 | }); 36 | }; 37 | 38 | render() { 39 | if (!this.props.keywords.length) { 40 | return false; 41 | } 42 | return ( 43 |
44 | {this.formatKeywords(this.props.keywords)} 45 |
46 | ); 47 | } 48 | 49 | } 50 | 51 | export default Visible()(Keywords); -------------------------------------------------------------------------------- /demo/src/components/Question.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component, PropTypes} from 'react'; 4 | import {hashHistory} from 'react-router'; 5 | import Radium from 'radium'; 6 | import Visible from 'visible-react'; 7 | 8 | import Keywords from './Keywords'; 9 | import Byline from './Byline'; 10 | import styles from '../styles/styles'; 11 | 12 | class Question extends Component { 13 | 14 | static propTypes = { 15 | id: PropTypes.string.isRequired, 16 | title: PropTypes.string.isRequired, 17 | user: PropTypes.string.isRequired, 18 | date: PropTypes.number.isRequired, 19 | keywords: PropTypes.array.isRequired, 20 | answers: PropTypes.array.isRequired 21 | }; 22 | 23 | styles = { 24 | title: { 25 | marginBottom: '5px', 26 | color: '#04aec3', 27 | fontSize: '16px', 28 | cursor: 'pointer', 29 | ':hover': { 30 | opacity: '.5' 31 | } 32 | } 33 | }; 34 | 35 | componentWillMount() { 36 | 37 | } 38 | 39 | handleQuestionClicked = () => { 40 | hashHistory.push('question/' + this.props.id); 41 | }; 42 | 43 | render() { 44 | const id = 'question-' + this.props.id; 45 | return ( 46 |
47 |
48 | {this.props.title} 49 |
50 | 51 | 57 |
58 | ); 59 | } 60 | 61 | } 62 | 63 | export default Radium(Visible()(Question)); -------------------------------------------------------------------------------- /demo/src/components/QuestionDetail.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component, PropTypes} from 'react'; 4 | import {connect} from 'react-redux'; 5 | import {hashHistory} from 'react-router'; 6 | import deepcopy from 'deepcopy'; 7 | import RaisedButton from 'material-ui/RaisedButton'; 8 | import Visible from 'visible-react'; 9 | 10 | import Keywords from './Keywords'; 11 | import Byline from './Byline'; 12 | import Answer from './Answer'; 13 | import SubmitAnswer from './SubmitAnswer'; 14 | 15 | import styles from '../styles/styles'; 16 | 17 | // Export unconnected component for testing 18 | export class QuestionDetail extends Component { 19 | 20 | styles = { 21 | title: { 22 | margin: '10px 0 5px 0', 23 | fontSize: '18px', 24 | fontWeight: 'bold' 25 | }, 26 | description: { 27 | paddingTop: '10px', 28 | borderTop: '1px solid lightgray', 29 | fontSize: '14px' 30 | } 31 | }; 32 | 33 | componentWillReceiveProps() { 34 | 35 | } 36 | 37 | componentWillMount() { 38 | // If the question ID in the URL is invalid, go to main page 39 | if (!this.getSelectedQuestion()) { 40 | hashHistory.push('/'); 41 | } 42 | } 43 | 44 | getSelectedQuestion = () => { 45 | return this.props.questions.find((question) => { 46 | return question.id === this.props.params.id; 47 | }); 48 | }; 49 | 50 | getAnswerHeading = (answerCount) => { 51 | let label = 'Answer'; 52 | if (answerCount !== 1) { 53 | label += 's'; 54 | } 55 | return ( 56 |
57 | {answerCount} {label} 58 |
59 | ) 60 | }; 61 | 62 | getAnswers = (answers) => { 63 | return answers.map((answer, ind) => { 64 | const id = 'answer-' + ind; 65 | return (); 66 | }); 67 | }; 68 | 69 | handleBackButtonClick = () => { 70 | hashHistory.push('/'); 71 | }; 72 | 73 | render() { 74 | const question = this.getSelectedQuestion(); 75 | if (!question) { 76 | return false; 77 | } 78 | return ( 79 |
80 | 86 |
Question
87 |
88 |
89 | {question.title} 90 |
91 | 92 | 93 |
{question.description}
94 |
95 | {this.getAnswerHeading(question.answers.length)} 96 | {this.getAnswers(question.answers)} 97 | 98 |
99 | ) 100 | } 101 | 102 | } 103 | 104 | export default connect( 105 | (state) => deepcopy(state) 106 | )(Visible()(QuestionDetail)); -------------------------------------------------------------------------------- /demo/src/components/QuestionList.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import {connect} from 'react-redux'; 5 | import {hashHistory} from 'react-router'; 6 | import deepcopy from 'deepcopy'; 7 | import RaisedButton from 'material-ui/RaisedButton'; 8 | import Visible from 'visible-react'; 9 | 10 | import Question from './Question'; 11 | import styles from '../styles/styles'; 12 | 13 | // Export unconnected component for testing 14 | export class QuestionList extends Component { 15 | 16 | getQuestionList = () => { 17 | return this.props.questions.map((question, ind) => { 18 | return ( 19 | 23 | ); 24 | }); 25 | }; 26 | 27 | showSubmitQuestion = () => { 28 | hashHistory.push('new-question'); 29 | }; 30 | 31 | render() { 32 | return ( 33 |
34 |
35 | 41 |
42 |
Questions
43 |
{this.getQuestionList()}
44 |
45 | ); 46 | } 47 | 48 | } 49 | 50 | export default connect( 51 | (state) => deepcopy(state) 52 | )(Visible({disabled: true, id: 'questionlist1'})(QuestionList)); -------------------------------------------------------------------------------- /demo/src/components/SubmitAnswer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component, PropTypes} from 'react'; 4 | import {connect} from 'react-redux'; 5 | import {hashHistory} from 'react-router'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | import Visible from 'visible-react'; 8 | 9 | import {addAnswer} from '../actions/actions'; 10 | import styles from '../styles/styles'; 11 | 12 | class SubmitAnswer extends Component { 13 | 14 | static propTypes = { 15 | questionId: PropTypes.string.isRequired 16 | }; 17 | 18 | styles = { 19 | container: { 20 | display: 'flex', 21 | flexDirection: 'column' 22 | }, 23 | textArea: { 24 | resize: 'vertical', 25 | marginBottom: '20px', 26 | border: '1px solid lightgray', 27 | outline: 'none' 28 | }, 29 | submitButton: { 30 | marginTop: '20px', 31 | fontSize: '12px', 32 | cursor: 'pointer' 33 | } 34 | }; 35 | 36 | constructor() { 37 | super(); 38 | this.state = { 39 | answerText: '' 40 | }; 41 | } 42 | 43 | componentDidMount() { 44 | console.log('CDM called in submit'); 45 | this.setState({ 46 | mounted: true 47 | }); 48 | } 49 | 50 | componentWillReceiveProps() { 51 | 52 | }; 53 | 54 | componentDidUpate() { 55 | 56 | }; 57 | 58 | updateEntry = (event) => { 59 | this.setState({ 60 | answerText: event.target.value 61 | }); 62 | }; 63 | 64 | handleSubmitClicked = () => { 65 | this.props.dispatch(addAnswer(this.props.questionId, this.state.answerText)); 66 | this.setState({ 67 | answerText: '' 68 | }); 69 | }; 70 | 71 | render() { 72 | const mounted = this.state.mounted; 73 | return ( 74 |
75 |
Post an Answer
76 |