Skip to content

Commit 72dda6c

Browse files
committed
Added an self-mounting components example.
These replicate much of the behaviour that was formerly available in python-react.
1 parent 056ca70 commit 72dda6c

16 files changed

Lines changed: 413 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ docs/_build/
5454
target/
5555

5656
node_modules
57-
example/static/webpack
57+
.webpack_build_cache
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
static
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
python + react - self mounting components
2+
=========================================
3+
4+
This example illustrates a workflow where webpack is used to generate bundles so that the root React
5+
component can immediately mount itself over the markup that was pre-rendered with the same data.
6+
7+
This workflow is similar to what was provided in older versions of python-react. It can be useful
8+
if you want to add a little interactivity to an otherwise backend-heavy site.
9+
10+
Be aware that while this workflow can be initially convenient, it tends to rely on components maintaining
11+
large amounts of state. A better workflow is for your components to minimize state by delegating all
12+
data storage to external services. If you're looking for something to handle your data, the multitude
13+
of Flux implementations are a reasonable starting point.
14+
15+
16+
### Running the example
17+
18+
Install the dependencies
19+
20+
```
21+
pip install -r requirements.txt
22+
npm install
23+
```
24+
25+
Start the server
26+
27+
```
28+
node server.js
29+
```
30+
31+
Start the python server
32+
33+
```
34+
python example.py
35+
```
36+
37+
And visit [http://127.0.0.1:5000](http://127.0.0.1:5000)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
3+
class Comment extends React.Component {
4+
render() {
5+
return (
6+
<div>
7+
<h3>{this.props.name}</h3>
8+
{this.props.text}
9+
</div>
10+
);
11+
}
12+
}
13+
14+
export default Comment;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
h2 {
2+
border-bottom: 1px solid #eee;
3+
}
4+
5+
form label {
6+
display: block;
7+
}
8+
9+
form button {
10+
margin-left: 5px;
11+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// CSS dependencies
2+
import 'bootstrap/dist/css/bootstrap.css';
3+
import './CommentBox.css';
4+
5+
import React from 'react';
6+
import CommentList from './CommentList.jsx';
7+
import CommentForm from './CommentForm.jsx';
8+
import $ from 'jquery';
9+
10+
class CommentBox extends React.Component {
11+
constructor(props) {
12+
super(props);
13+
14+
this.state = {comments: props.comments};
15+
}
16+
submitComment(name, text) {
17+
$.ajax({
18+
url: this.props.url,
19+
method: 'post',
20+
data: {
21+
name,
22+
text
23+
},
24+
success: (obj) => {
25+
this.setState({
26+
comments: obj.comments
27+
});
28+
},
29+
error: (err) => {
30+
console.error(err);
31+
}
32+
});
33+
}
34+
render() {
35+
return (
36+
<div>
37+
<CommentList comments={this.state.comments} />
38+
<CommentForm submitComment={this.submitComment.bind(this)} />
39+
</div>
40+
);
41+
}
42+
}
43+
44+
export default CommentBox;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
3+
class CommentForm extends React.Component {
4+
handleSubmit(event) {
5+
event.preventDefault();
6+
7+
var name = this.refs.name.getDOMNode().value.trim();
8+
var text = this.refs.text.getDOMNode().value.trim();
9+
10+
this.props.submitComment(name, text);
11+
}
12+
render() {
13+
return (
14+
<form onSubmit={this.handleSubmit.bind(this)}>
15+
<h2>Submit a comment</h2>
16+
<div className="form-group">
17+
<label>
18+
Your name
19+
<input ref="name" type="text" className="form-control" placeholder="..." />
20+
</label>
21+
</div>
22+
<div className="form-group">
23+
<label>
24+
Say something...
25+
<textarea ref="text" className="form-control" placeholder="..." />
26+
</label>
27+
</div>
28+
<div className="text-right">
29+
<button type="reset" className="btn btn-default">Reset</button>
30+
<button type="submit" className="btn btn-primary">Submit</button>
31+
</div>
32+
</form>
33+
);
34+
}
35+
}
36+
37+
export default CommentForm;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react';
2+
import Comment from './Comment.jsx';
3+
4+
class CommentList extends React.Component {
5+
render() {
6+
if (!this.props.comments.length) {
7+
return null;
8+
}
9+
return (
10+
<div>
11+
<h2>Comments</h2>
12+
{this.props.comments.map((comment, index) => {
13+
return <Comment name={comment.name} text={comment.text} key={index} />;
14+
})}
15+
</div>
16+
);
17+
}
18+
}
19+
20+
export default CommentList;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import os
2+
import json
3+
from flask import Flask, render_template, request, redirect, jsonify
4+
from react.conf import settings as react_settings
5+
from react.render import render_component
6+
from webpack.conf import settings as webpack_settings
7+
from webpack.compiler import webpack
8+
9+
DEBUG = True
10+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
11+
12+
# As a convenience for development, only connect to the
13+
# render server when DEBUG is False
14+
react_settings.configure(RENDER=not DEBUG)
15+
16+
webpack_settings.configure(
17+
STATIC_ROOT=os.path.join(BASE_DIR, 'static'),
18+
STATIC_URL='/static/',
19+
WATCH=DEBUG,
20+
HMR=DEBUG,
21+
CONFIG_DIRS=BASE_DIR,
22+
CONTEXT={
23+
'DEBUG': DEBUG,
24+
},
25+
)
26+
27+
28+
app = Flask(__name__)
29+
app.debug = DEBUG
30+
31+
comments = []
32+
33+
34+
@app.route('/')
35+
def index():
36+
config_file = os.path.join(BASE_DIR, 'example.webpack.js')
37+
38+
component = os.path.join(BASE_DIR, 'app', 'CommentBox.jsx')
39+
40+
props = {
41+
'comments': comments,
42+
'url': '/comment/',
43+
}
44+
45+
rendered = render_component(component, props)
46+
47+
webpack_context = {
48+
'component': component,
49+
'props_var': 'window.mountProps',
50+
'container': 'mount-container',
51+
}
52+
53+
bundle = webpack(config_file, context=webpack_context)
54+
55+
return render_template(
56+
'index.html',
57+
bundle=bundle,
58+
webpack_context=webpack_context,
59+
rendered=rendered,
60+
)
61+
62+
63+
@app.route('/comment/', methods=('POST',))
64+
def comment():
65+
comments.append({
66+
'name': request.form['name'],
67+
'text': request.form['text'],
68+
})
69+
70+
if request.is_xhr:
71+
return jsonify(comments=comments)
72+
73+
return redirect('/')
74+
75+
76+
77+
if __name__ == '__main__':
78+
app.run()
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
var path = require('path');
2+
var webpack = require('webpack');
3+
var autoprefixer = require('autoprefixer-core');
4+
var ExtractTextPlugin = require('extract-text-webpack-plugin');
5+
6+
module.exports = function(opts) {
7+
var config = {
8+
context: __dirname,
9+
entry: './mount',
10+
output: {
11+
filename: '[name]-[hash].js',
12+
pathinfo: opts.context.DEBUG
13+
},
14+
module: {
15+
loaders: [
16+
{
17+
test: /\.jsx?$/,
18+
exclude: /(node_modules|bower_components)/,
19+
loader: (opts.hmr ? 'react-hot-loader!': '') + 'babel-loader'
20+
},
21+
{
22+
test: /\.css$/,
23+
loader: opts.hmr ?
24+
'style!css-loader?sourceMap!postcss-loader' :
25+
ExtractTextPlugin.extract('style', 'css-loader?sourceMap!postcss-loader')
26+
},
27+
{
28+
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
29+
loader: 'url-loader?limit=10000&mimetype=application/font-woff'
30+
},
31+
{
32+
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
33+
loader: 'file-loader'
34+
}
35+
]
36+
},
37+
postcss: [autoprefixer],
38+
resolve: {
39+
alias: {
40+
__react_mount_component__: opts.context.component
41+
}
42+
},
43+
plugins: [
44+
// Define the variables in `./mount.js` that webpack will replace with data from python
45+
new webpack.DefinePlugin({
46+
__react_mount_props_variable__: opts.context.props_var,
47+
__react_mount_container__: JSON.stringify(opts.context.container)
48+
}),
49+
new webpack.optimize.OccurrenceOrderPlugin(),
50+
new webpack.NoErrorsPlugin(),
51+
new webpack.DefinePlugin({
52+
'process.env': {
53+
NODE_ENV: JSON.stringify(
54+
opts.context.DEBUG ? 'development' : 'production'
55+
)
56+
}
57+
})
58+
],
59+
devtool: opts.context.DEBUG ? 'eval-source-map' : 'source-map'
60+
};
61+
62+
if (!opts.hmr) {
63+
// Move css assets into separate files
64+
config.plugins.push(new ExtractTextPlugin('[name]-[contenthash].css'));
65+
}
66+
67+
if (!opts.context.DEBUG) {
68+
// Remove duplicates and activate compression
69+
config.plugins.push(
70+
new webpack.optimize.DedupePlugin(),
71+
new webpack.optimize.UglifyJsPlugin()
72+
);
73+
}
74+
75+
return config;
76+
};

0 commit comments

Comments
 (0)