diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..ea85739 --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + "plugins": [ + "transform-react-jsx" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index d685e3d..ecffdad 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,4 @@ docs/_build/ target/ node_modules -example/static/webpack \ No newline at end of file +.webpack_build_cache \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index eba3223..a61f73b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,27 @@ language: python python: - "2.7" - - "3.4" + - "3.5" + - "3.7" + - "3.8" env: - DJANGO_VERSION=1.7.8 - DJANGO_VERSION=1.8.1 + - DJANGO_VERSION=2.2.16 + - DJANGO_VERSION=3.1.1 + - DJANGO_VERSION=1.8.19 install: + # Install node 6 (current LTS as of January 2017) + - "rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install 6" + # Install latest npm - "npm install -g npm" + # Install node deps for render server - "cd tests" - "npm install" - "cd .." + # Setup python packages - "pip install Django==$DJANGO_VERSION" - "pip install -r requirements.txt" diff --git a/CHANGELOG.md b/CHANGELOG.md index bef46b9..2d309cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,112 @@ Changelog ========= + +### 4.3.0 (8/10/2018) + +- Add option to pass a URL explicitly instead of reading from settings. + ([jchen-eb](https://github.com/jchen-eb)) + https://github.com/markfinger/python-react/pull/88 + + +### 4.2.1 (17/6/2018) + +- Fix for a missing argument bug that could occur with rendering deactivated. + + +### 4.2.0 (17/6/2018) + +- API for returning extra items returned by render server. + ([Sassan Haradji](https://github.com/sassanh)) + https://github.com/markfinger/python-react/pull/87 + + +### 4.1.1 (9/8/2017) + +- Update timeout in render_server.py. + ([Mike Plis](https://github.com/mplis)) + https://github.com/markfinger/python-react/pull/81 + + +### 4.1.0 (1/3/2017) + +- `render_component` now accepts a `timeout` keyword argument which is passed to `RenderServer.render`. + ([Corey Burmeister](https://github.com/cburmeister)) + https://github.com/markfinger/python-react/pull/74 + + +### 4.0.0 (28/2/2017) + +- **Possibly breaking change** `RenderServer.render` now accepts a `timeout` keyword argument. There are some + edge-cases where this may break down-stream code. + ([Corey Burmeister](https://github.com/cburmeister)) + https://github.com/markfinger/python-react/pull/73 +- Documentation updates regarding production environments. The key takeaway is to ensure that you are using + the `NODE_ENV=production` environment variable so that React runs without debugging helpers which slow down + rendering. +- Documentation updates regarding `RenderServer` API. + + +### 3.0.1 (6/4/2016) + +- Documentation updates. + + +### 3.0.0 (6/4/2016) + +- **Possibly breaking change** render_component now accepts a `request_headers` keyword argument. + There are some edge-cases where this may break down-stream code. If you are overriding + part of the render pipeline, you may need to ensure that you are using `**kwargs` to read and/or pass wildcard arguments. + ([Ben Ilegbodu](https://github.com/benmvp)) + https://github.com/markfinger/python-react/pull/64 +- [Documentation] Fix outdated link to server.js + ([Jonathan Cox](https://github.com/geezhawk)) + https://github.com/markfinger/python-react/pull/60 +- [Examples] missing babel-preset-es2015 in package.json in Tornado-example + ([付雨帆](https://github.com/letfly)) + https://github.com/markfinger/python-react/pull/59 +- [Examples] Added missing dependency on babel-preset-es2015 + ([Rune Juhl Jacobsen](https://github.com/runejuhl)) + https://github.com/markfinger/python-react/pull/56 +- [Examples] Added es6 compiler plugin to .bablerc in basic_rendering + ([Pringels](https://github.com/Pringels)) + https://github.com/markfinger/python-react/pull/55 + + +### 2.0.0 (22/9/2015) + +- **Breaking change** The base renderer's __init__ no longer accepts the RENDER_URL setting as an argument. + The url is now resolved during calls, rather than initialisation. +- When used in companion with Django, settings will now be dynamically fetched rather than bound on + initialisation. This enables a codebase to be more easily controlled from a test suite +- Updated docs regarding front-end integration + + +### 1.0.0 (13/7/2015) + +- Removed the webpack integration. While it can be initially convenient, it tends to introduce more problems than + it solves. The repo contains an example illustrating how to implement self-mounting components which provide + similar functionality to the former webpack integration. +- Replaced the js-host dependency with an externally-managed render server. +- Added a `renderer` hook on `render_component`. Enabling you to override the default which assumes + [render-react](https://github.com/markfinger/react-render) + + +### 0.13.1 (16/5/2015) + +- Fixed a potential path issue in config files +- Replaced the webpack-service dependency with webpack-wrapper. + + ### 0.8.0 (26/1/2015) - Boosting render performance by using a dedicated render server. -- Added a new setting, DJANGO_REACT['RENDERER'], which is a string denoting an import path to a +- Added a new setting, DJANGO_REACT['RENDERER'], which is a string denoting an import path to a callable object which returns a on object with a `render` method. By default it points to the new - render server, 'django_react.render_server.ReactRenderServer'. The legacy renderer is useable by + render server, 'django_react.render_server.ReactRenderServer'. The legacy renderer is useable by setting DJANGO_REACT['RENDERER'] = 'django_react.renderer.ReactRenderer'. + ### 0.7.0 (2/1/2015) - Changed `django_react.exceptions.ReactComponentMissingSourceAttribute` to `django_react.exceptions.ReactComponentMissingSource` @@ -19,10 +117,12 @@ Changelog - The Python<->JS bridge used to render components now relies on a `--serialized-props-file` argument, formerly it was `--serialized-props`. - Switched the JSX loader to a fork which improves the debug information provided during error handling + ### 0.6.0 (24/12/2014) - The NODE_ENV environment setting is now controlled by the `DJANGO_REACT['DEBUG']` setting. Activating it will provides some improvements to the rendering performance. + ### 0.5.0 (14/12/2014) - Renamed `django_react.exceptions.PropSerialisationError` to `django_react.exceptions.PropSerializationError`. @@ -44,6 +144,7 @@ Changelog - Added a test suite and harness. - Added basic documentation. + ### 0.4.0 (11/12/2014) - Fixed a bug where errors caused during a component's prop serialization could silently fail. @@ -56,6 +157,7 @@ Changelog - `ReactComponent.get_component_variable` is now `ReactComponent.get_library`. - Moved the Webpack configuration into the ReactComponent class. + ### 0.3.0 (3/12/2014) - `django_react.exceptions.ReactComponentSourceFileNotFound` is now `django_react.exceptions.SourceFileNotFound` @@ -66,10 +168,12 @@ Changelog - `django_react.utils.render` no longer accepts a `ReactComponent` as an argument, it now takes `path_to_source`, `serialised_props`, and `to_static_markup`. - `django_react/render.js` no longer accepts the `--path-to-component` argument, instead it takes `--path-to-source`. + ### 0.2.0 (3/12/2014) - Replaced the post-install step in setup.py with django-node's dependency and package resolver. + ### 0.1.0 (2/12/2014) - Initial release diff --git a/README.md b/README.md index 0b38658..b73e30c 100644 --- a/README.md +++ b/README.md @@ -3,99 +3,128 @@ python-react [![Build Status](https://travis-ci.org/markfinger/python-react.svg?branch=master)](https://travis-ci.org/markfinger/python-react) -Server-side rendering, client-side mounting, JSX translation, and component bundling. +Server-side rendering of React components with data from your Python system ```python from react.render import render_component -component = render_component( - # A path to a file exporting your React component +rendered = render_component( '/path/to/component.jsx', - # Translate the source to JavaScript from JSX + ES6/7 - translate=True, - # Props that will be passed to the renderer and reused on - # the client-side to provide immediate interactivity - props={ + { 'foo': 'bar', 'woz': [1,2,3], } ) -# The rendered markup -print(component) - -# Outputs JavaScript which will mount the component on the client-side and -# provide immediate interactivity -print(component.render_js()) +print(rendered) ``` -[render_component](#render_component) is the main entry point to pre-render components and package them for -use on the client-side. - -If you only want to use your JSX files on the client-side, you can use [bundle_component](#bundle_component) -to translate and bundle the source code into a form that will run in a browser. +For client-side integrations, refer to the [docs](#using-react-on-the-front-end). Documentation ------------- - [Installation](#installation) -- [API](#api) - - [render_component()](#render_component) - - [bundle_component()](#bundle_component) - - [RenderedComponent](#renderedcomponent) -- [Django integration](#django-integration) +- [Basic usage](#basic-usage) + - [Setting up a render server](#setting-up-a-render-server) +- [Using React on the front-end](#using-react-on-the-front-end) +- [render_component](#render_component) +- [Render server](#render-server) + - [Usage in development](#usage-in-development) + - [Usage in production](#usage-in-production) + - [Overriding the renderer](#overriding-the-renderer) - [Settings](#settings) +- [Frequently Asked Questions](#frequently-asked-questions) - [Running the tests](#running-the-tests) Installation ------------ -python-react depends on [js-host](https://github.com/markfinger/python-js-host/) to provide -interoperability with JavaScript. Complete its -[quick start](https://github.com/markfinger/python-js-host/#quick-start) before continuing. +```bash +pip install react +``` + -Install [python-webpack](https://github.com/markfinger/python-webpack) +Basic usage +----------- -Install python-react's JS dependencies +python-react provides an interface to a render server which is capable of rendering React components with data +from your python process. -```bash -npm install --save react react-render babel-core babel-loader -``` +Render requests should provide a path to a JS file that exports a React component. If you want to pass +data to the component, you can optionally provide a second argument that will be used as the component's +`props` property. -Add react-render to the functions definition of your `host.config.js` file +```python +from react.render import render_component -```javascript -var reactRender = require('react-render'); +rendered = render_component('path/to/component.jsx', {'foo': 'bar'}) -module.exports = { - functions: { - // ... - react: reactRender - } -}; +print(rendered) ``` -And install python-react +The object returned has three properties: -```bash -pip install react -``` + - `markup` - the rendered markup + - `props` - the JSON-serialized props + - `data` - the data returned by the render server +If the object is coerced to a string, it will emit the value of the `markup` attribute. -API ---- +### Setting up a render server -### render_component() +Render servers are typically Node.js processes which sit alongside the python process and respond to network requests. -Renders a component to its initial HTML. You can use this method to generate HTML on the server -and send the markup down on the initial request for faster page loads and to allow search engines -to crawl your pages for SEO purposes. +To add a render server to your project, you can refer to the [basic rendering example](examples/basic_rendering) +for a simple server that will cover most cases. The key files for the render server are: + - [render_server.js](examples/basic_rendering/render_server.js) - the server's source code + - [package.json](examples/basic_rendering/package.json) - the server's dependencies, installable with + [npm](http://npmjs.com) + + +Using React on the front-end +---------------------------- + +There are a number of ways in which you can integrate React into the frontend of a Python system. The typical +setup involves a build tool and a python package that can integrate it. + +The two most popular build tools are: + +- [Webpack](https://webpack.github.io) - compiles your files into browser-executable code and provides a + variety of tools and processes which can simplify complicated workflows. +- [Browserify](http://browserify.org/) - has a lot of cross-over with webpack. Is argurably the easiest of the + two to use, but it tends to lag behind webpack in functionality. -Returns a [RenderedComponent](#renderedcomponent) instance which can be passed directly into your -front end to output the component's markup and to mount the component for client-side interactivity. +For React projects, you'll find that webpack is the usual recommendation. Webpack's hot module replacement, +code-splitting, and a wealth of loaders are the features typically cited as being irreplaceable. +[react-hot-loader](https://github.com/gaearon/react-hot-loader) is a particularly useful tool, as it allows +changes to your components to be streamed live into your browser. + +If you want to integrate webpack's output into your python system, you can either hard-code the paths or you +can use a manifest plugin that provides a way for your python system to introspect the compiler's state. + +The most popular manifest tool is [owais/django-webpack-loader](https://github.com/owais/django-webpack-loader). +Owais has provided a great set of docs and examples, so it's your best bet for integrating webpack into your +project. + +If you aren't running a Django system, or you need portable manifests that can be decoupled and deployed, +[markfinger/python-webpack-manifest](https://github.com/markfinger/python-webpack-manifest) might suit your +needs. + +There's also [markfinger/python-webpack](https://github.com/markfinger/python-webpack), but it's a bit more +heavy handed, abstract, and is only of use if you need a really tight coupling between your python and +javascript worlds. + + +render_component +---------------- + +Renders a component to its initial HTML. You can use this method to generate HTML on the server +and send the markup down on the initial request for faster page loads and to allow search engines +to crawl your pages for SEO purposes. #### Usage @@ -106,191 +135,214 @@ from react.render import render_component render_component( # A path to a file which exports your React component path='...', + # An optional dictionary of data that will be passed to the renderer - # and can be reused on the client-side - props = { + # and can be reused on the client-side. + props={ 'foo': 'bar' }, - # An optional boolean indicating that the component should be bundled and - # translated from JSX and ES6/7 before rendering. Components are translated - # with Babel - translate = True, - # An optional boolean indicating that the component should be bundled for - # before rendering. If `translate` is set to True, this argument is ignored - bundle = True, - # An optional boolean indicating that React's `renderToStaticMarkup` method + + # An optional boolean indicating that React's `renderToStaticMarkup` method # should be used, rather than `renderToString` - to_static_markup = False, - # An optional class which is used to encode the props to JSON - json_encoder=None, + to_static_markup=False, + + # An optional object which will be used instead of the default renderer + renderer=None, + + # An optional dictionary of request header information (such as `Accept-Language`) + # to pass along with the request to the render server + request_headers={ + 'Accept-Language': 'da, en-gb;q=0.8, en;q=0.7' + }, + + # An optional timeout that is used when handling communications with the render server. + # Can be an integer, a float, or a tuple containing two numeric values (the two values + # represent the individual timeouts on the send & receive phases of the request). + # Note that if not defined, this value will default to (5, 5) + timeout=None ) ``` +If you are using python-react in a Django project, relative paths to components will be resolved +via Django's static file finders. -### bundle_component() +By default, render_component relies on access to a render server that exposes an endpoint compatible +with [react-render's API](https://github.com/markfinger/react-render). If you want to use a different +renderer, pass in an object as the `renderer` arg. The object should expose a `render` method which +accepts the `path`, `data`, `to_static_markup`, and `request_headers` arguments. -Packages a React component so that it can be re-used on the client-side. JSX + ES6+7 files are translated -to JavaScript with [Babel](https://babeljs.io/). -Be aware that `bundle_component` is primarily a convenience method. Under the hood, it simply generates a webpack -config file which is passed to [python-webpack](https://github.com/markfinger/python-webpack). +Render server +------------- -If you require more flexibility in the bundling process, you are recommended to read the code and then use -python-webpack directly. +Earlier versions of this library would run the render server as a subprocess, this tended to make development +easier, but introduced instabilities and opaque behaviour. To avoid these issues python-react now relies on +externally managed process. While managing extra processes can add more overhead initially, it avoids pain down +the track. +If you only want to run the render server in particular environments, change the `RENDER` setting to +False. When `RENDER` is False, the render server is not used directly, but it's wrapper will return similar +objects with the `markup` attribute as an empty string. -#### Usage -```python -from react.bundle import bundle_component +### Usage in development -bundle = bundle_component( - # A path to a file which exports the component. If the path is relative, - # django's static file finders will attempt to find the file - path='...', - # An optional boolean indicating that the component should be translated - # from JSX and ES6/7 during the bundling process - translate = True, -) +In development environments, it can be easiest to set the `RENDER` setting to False. This ensures that the +render server will not be used, hence you only need to manage your python process. -# Render a - - - {{ comment_box.render_js()|safe }} - {% endif %} - - \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..628372b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,17 @@ +Python + React examples +===================== + +[Basic rendering](basic_rendering) + +Illustrates how to pre-render React components for a production system. The example use React as a substitute for +python template systems. + +------------------------------------ + +For client-side integration examples, +[Owais's django-webpack-loader project](https://github.com/owais/django-webpack-loader/tree/master/examples) +has a bunch of good starting points. + +------------------------------------ + +*Feel free to open a pull request, if you'd like to contribute more examples or links* diff --git a/examples/Tornado-example/.babelrc b/examples/Tornado-example/.babelrc new file mode 100644 index 0000000..3bfb24f --- /dev/null +++ b/examples/Tornado-example/.babelrc @@ -0,0 +1,6 @@ +{ + "plugins": [ + "transform-react-jsx" + ], + "presets": ["es2015"] +} diff --git a/examples/Tornado-example/README.md b/examples/Tornado-example/README.md new file mode 100644 index 0000000..9e3f37c --- /dev/null +++ b/examples/Tornado-example/README.md @@ -0,0 +1,23 @@ +Running the example +=================== + +Install the dependencies + +``` +pip install -r requirements.txt +npm install +``` + +Start the render server + +``` +node server.js +``` + +Start the python server + +``` +python app.py +``` + +And visit [http://127.0.0.1:8000](http://127.0.0.1:8000) diff --git a/examples/Tornado-example/app.py b/examples/Tornado-example/app.py new file mode 100644 index 0000000..160c75a --- /dev/null +++ b/examples/Tornado-example/app.py @@ -0,0 +1,58 @@ +import os +import tornado.ioloop +import tornado.httpserver +from tornado.web import RequestHandler +from tornado.gen import coroutine +from react.render import render_component + + +comments = [] + +class IndexHandler(RequestHandler): + @coroutine + def get(self): + rendered = render_component( + os.path.join(os.getcwd(), 'static', 'js', 'CommentBox.jsx'), + { + 'comments': comments, + 'url': '/comments', + 'xsrf':self.xsrf_token + }, + to_static_markup=False, + ) + self.render('index.html', rendered=rendered) + + +class CommentHandler(RequestHandler): + @coroutine + def post(self): + comments.append({ + 'author': self.get_argument('author'), + 'text': self.get_argument('text'), + }) + self.redirect('/') + + +urls = [ + (r"/", IndexHandler), + (r"/comments", CommentHandler), + (r"/(.*)", tornado.web.StaticFileHandler, {"path":r"{0}".format(os.path.join(os.path.dirname(__file__),"static"))}), +] + +settings = dict({ + "template_path": os.path.join(os.path.dirname(__file__),"templates"), + "static_path": os.path.join(os.path.dirname(__file__),"static"), + "cookie_secret": os.urandom(12), + "xsrf_cookies": True, + "debug": True, + "compress_response": True +}) + +application = tornado.web.Application(urls,**settings) + + +if __name__ == "__main__": + server = tornado.httpserver.HTTPServer(application) + server.listen(8000) + tornado.ioloop.IOLoop.instance().start() + diff --git a/examples/Tornado-example/package.json b/examples/Tornado-example/package.json new file mode 100644 index 0000000..d8f68c1 --- /dev/null +++ b/examples/Tornado-example/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "dependencies": { + "babel-core": "^6.1.20", + "babel-plugin-transform-react-jsx": "^6.1.18", + "babel-preset-es2015": "^6.3.13", + "body-parser": "^1.14.1", + "express": "^4.13.3", + "react": "^0.14.2", + "react-dom": "^0.14.2", + "react-render": "^1.1.0", + "yargs": "^3.29.0" + } +} diff --git a/examples/Tornado-example/requirements.txt b/examples/Tornado-example/requirements.txt new file mode 100644 index 0000000..221f73d --- /dev/null +++ b/examples/Tornado-example/requirements.txt @@ -0,0 +1,2 @@ +tornado +react diff --git a/examples/Tornado-example/server.js b/examples/Tornado-example/server.js new file mode 100644 index 0000000..c1cb8bb --- /dev/null +++ b/examples/Tornado-example/server.js @@ -0,0 +1,40 @@ +var http = require('http'); +var express = require('express'); +var bodyParser = require('body-parser'); +var reactRender = require('react-render'); + +// Ensure support for JSX files +require('babel-core/register'); + +var ADDRESS = '127.0.0.1'; +var PORT = 9009; + +var app = express(); +var server = http.Server(app); + +app.use(bodyParser.json()); + +app.get('/', function(req, res) { + res.end('react render server'); +}); + +app.post('/render', function(req, res) { + reactRender(req.body, function(err, markup) { + var error = null; + if (err) { + error = { + type: err.constructor.name, + message: err.message, + stack: err.stack + }; + } + res.json({ + error: error, + markup: markup + }); + }); +}); + +server.listen(PORT, ADDRESS, function() { + console.log('react render server listening at http://' + ADDRESS + ':' + PORT); +}); diff --git a/examples/Tornado-example/static/css/app.css b/examples/Tornado-example/static/css/app.css new file mode 100644 index 0000000..ab19de0 --- /dev/null +++ b/examples/Tornado-example/static/css/app.css @@ -0,0 +1,11 @@ +h2 { + border-bottom: 1px solid #eee; +} + +form label { + display: block; +} + +form button { + margin-left: 5px; +} diff --git a/example/static/vendor/bootstrap/css/bootstrap.css b/examples/Tornado-example/static/css/bootstrap.css similarity index 94% rename from example/static/vendor/bootstrap/css/bootstrap.css rename to examples/Tornado-example/static/css/bootstrap.css index c46af7d..680e768 100644 --- a/example/static/vendor/bootstrap/css/bootstrap.css +++ b/examples/Tornado-example/static/css/bootstrap.css @@ -1,10 +1,9 @@ /*! - * Bootstrap v3.3.2 (http://getbootstrap.com) + * Bootstrap v3.3.5 (http://getbootstrap.com) * Copyright 2011-2015 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ - -/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ html { font-family: sans-serif; -webkit-text-size-adjust: 100%; @@ -239,9 +238,6 @@ th { h3 { page-break-after: avoid; } - select { - background: #fff !important; - } .navbar { display: none; } @@ -958,12 +954,24 @@ th { .glyphicon-bitcoin:before { content: "\e227"; } +.glyphicon-btc:before { + content: "\e227"; +} +.glyphicon-xbt:before { + content: "\e227"; +} .glyphicon-yen:before { content: "\00a5"; } +.glyphicon-jpy:before { + content: "\00a5"; +} .glyphicon-ruble:before { content: "\20bd"; } +.glyphicon-rub:before { + content: "\20bd"; +} .glyphicon-scale:before { content: "\e230"; } @@ -1161,6 +1169,9 @@ hr { overflow: visible; clip: auto; } +[role="button"] { + cursor: pointer; +} h1, h2, h3, @@ -1329,62 +1340,72 @@ mark, .text-primary { color: #337ab7; } -a.text-primary:hover { +a.text-primary:hover, +a.text-primary:focus { color: #286090; } .text-success { color: #3c763d; } -a.text-success:hover { +a.text-success:hover, +a.text-success:focus { color: #2b542c; } .text-info { color: #31708f; } -a.text-info:hover { +a.text-info:hover, +a.text-info:focus { color: #245269; } .text-warning { color: #8a6d3b; } -a.text-warning:hover { +a.text-warning:hover, +a.text-warning:focus { color: #66512c; } .text-danger { color: #a94442; } -a.text-danger:hover { +a.text-danger:hover, +a.text-danger:focus { color: #843534; } .bg-primary { color: #fff; background-color: #337ab7; } -a.bg-primary:hover { +a.bg-primary:hover, +a.bg-primary:focus { background-color: #286090; } .bg-success { background-color: #dff0d8; } -a.bg-success:hover { +a.bg-success:hover, +a.bg-success:focus { background-color: #c1e2b3; } .bg-info { background-color: #d9edf7; } -a.bg-info:hover { +a.bg-info:hover, +a.bg-info:focus { background-color: #afd9ee; } .bg-warning { background-color: #fcf8e3; } -a.bg-warning:hover { +a.bg-warning:hover, +a.bg-warning:focus { background-color: #f7ecb5; } .bg-danger { background-color: #f2dede; } -a.bg-danger:hover { +a.bg-danger:hover, +a.bg-danger:focus { background-color: #e4b9b9; } .page-header { @@ -2564,10 +2585,13 @@ output { .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { - cursor: not-allowed; background-color: #eee; opacity: 1; } +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} textarea.form-control { height: auto; } @@ -2575,10 +2599,10 @@ input[type="search"] { -webkit-appearance: none; } @media screen and (-webkit-min-device-pixel-ratio: 0) { - input[type="date"], - input[type="time"], - input[type="datetime-local"], - input[type="month"] { + input[type="date"].form-control, + input[type="time"].form-control, + input[type="datetime-local"].form-control, + input[type="month"].form-control { line-height: 34px; } input[type="date"].input-sm, @@ -2634,6 +2658,7 @@ input[type="search"] { } .radio-inline, .checkbox-inline { + position: relative; display: inline-block; padding-left: 20px; margin-bottom: 0; @@ -2667,6 +2692,7 @@ fieldset[disabled] .checkbox label { cursor: not-allowed; } .form-control-static { + min-height: 34px; padding-top: 7px; padding-bottom: 7px; margin-bottom: 0; @@ -2698,17 +2724,18 @@ select[multiple].input-sm { line-height: 1.5; border-radius: 3px; } -select.form-group-sm .form-control { +.form-group-sm select.form-control { height: 30px; line-height: 30px; } -textarea.form-group-sm .form-control, -select[multiple].form-group-sm .form-control { +.form-group-sm textarea.form-control, +.form-group-sm select[multiple].form-control { height: auto; } .form-group-sm .form-control-static { height: 30px; - padding: 5px 10px; + min-height: 32px; + padding: 6px 10px; font-size: 12px; line-height: 1.5; } @@ -2734,17 +2761,18 @@ select[multiple].input-lg { line-height: 1.3333333; border-radius: 6px; } -select.form-group-lg .form-control { +.form-group-lg select.form-control { height: 46px; line-height: 46px; } -textarea.form-group-lg .form-control, -select[multiple].form-group-lg .form-control { +.form-group-lg textarea.form-control, +.form-group-lg select[multiple].form-control { height: auto; } .form-group-lg .form-control-static { height: 46px; - padding: 10px 16px; + min-height: 38px; + padding: 11px 16px; font-size: 18px; line-height: 1.3333333; } @@ -2766,12 +2794,16 @@ select[multiple].form-group-lg .form-control { text-align: center; pointer-events: none; } -.input-lg + .form-control-feedback { +.input-lg + .form-control-feedback, +.input-group-lg + .form-control-feedback, +.form-group-lg .form-control + .form-control-feedback { width: 46px; height: 46px; line-height: 46px; } -.input-sm + .form-control-feedback { +.input-sm + .form-control-feedback, +.input-group-sm + .form-control-feedback, +.form-group-sm .form-control + .form-control-feedback { width: 30px; height: 30px; line-height: 30px; @@ -2957,11 +2989,13 @@ select[multiple].form-group-lg .form-control { @media (min-width: 768px) { .form-horizontal .form-group-lg .control-label { padding-top: 14.333333px; + font-size: 18px; } } @media (min-width: 768px) { .form-horizontal .form-group-sm .control-label { padding-top: 6px; + font-size: 12px; } } .btn { @@ -3011,21 +3045,32 @@ select[multiple].form-group-lg .form-control { .btn.disabled, .btn[disabled], fieldset[disabled] .btn { - pointer-events: none; cursor: not-allowed; filter: alpha(opacity=65); -webkit-box-shadow: none; box-shadow: none; opacity: .65; } +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} .btn-default { color: #333; background-color: #fff; border-color: #ccc; } -.btn-default:hover, .btn-default:focus, -.btn-default.focus, +.btn-default.focus { + color: #333; + background-color: #e6e6e6; + border-color: #8c8c8c; +} +.btn-default:hover { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { @@ -3033,6 +3078,19 @@ fieldset[disabled] .btn { background-color: #e6e6e6; border-color: #adadad; } +.btn-default:active:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #333; + background-color: #d4d4d4; + border-color: #8c8c8c; +} .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { @@ -3068,9 +3126,17 @@ fieldset[disabled] .btn-default.active { background-color: #337ab7; border-color: #2e6da4; } -.btn-primary:hover, .btn-primary:focus, -.btn-primary.focus, +.btn-primary.focus { + color: #fff; + background-color: #286090; + border-color: #122b40; +} +.btn-primary:hover { + color: #fff; + background-color: #286090; + border-color: #204d74; +} .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { @@ -3078,6 +3144,19 @@ fieldset[disabled] .btn-default.active { background-color: #286090; border-color: #204d74; } +.btn-primary:active:hover, +.btn-primary.active:hover, +.open > .dropdown-toggle.btn-primary:hover, +.btn-primary:active:focus, +.btn-primary.active:focus, +.open > .dropdown-toggle.btn-primary:focus, +.btn-primary:active.focus, +.btn-primary.active.focus, +.open > .dropdown-toggle.btn-primary.focus { + color: #fff; + background-color: #204d74; + border-color: #122b40; +} .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { @@ -3113,9 +3192,17 @@ fieldset[disabled] .btn-primary.active { background-color: #5cb85c; border-color: #4cae4c; } -.btn-success:hover, .btn-success:focus, -.btn-success.focus, +.btn-success.focus { + color: #fff; + background-color: #449d44; + border-color: #255625; +} +.btn-success:hover { + color: #fff; + background-color: #449d44; + border-color: #398439; +} .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { @@ -3123,6 +3210,19 @@ fieldset[disabled] .btn-primary.active { background-color: #449d44; border-color: #398439; } +.btn-success:active:hover, +.btn-success.active:hover, +.open > .dropdown-toggle.btn-success:hover, +.btn-success:active:focus, +.btn-success.active:focus, +.open > .dropdown-toggle.btn-success:focus, +.btn-success:active.focus, +.btn-success.active.focus, +.open > .dropdown-toggle.btn-success.focus { + color: #fff; + background-color: #398439; + border-color: #255625; +} .btn-success:active, .btn-success.active, .open > .dropdown-toggle.btn-success { @@ -3158,9 +3258,17 @@ fieldset[disabled] .btn-success.active { background-color: #5bc0de; border-color: #46b8da; } -.btn-info:hover, .btn-info:focus, -.btn-info.focus, +.btn-info.focus { + color: #fff; + background-color: #31b0d5; + border-color: #1b6d85; +} +.btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { @@ -3168,6 +3276,19 @@ fieldset[disabled] .btn-success.active { background-color: #31b0d5; border-color: #269abc; } +.btn-info:active:hover, +.btn-info.active:hover, +.open > .dropdown-toggle.btn-info:hover, +.btn-info:active:focus, +.btn-info.active:focus, +.open > .dropdown-toggle.btn-info:focus, +.btn-info:active.focus, +.btn-info.active.focus, +.open > .dropdown-toggle.btn-info.focus { + color: #fff; + background-color: #269abc; + border-color: #1b6d85; +} .btn-info:active, .btn-info.active, .open > .dropdown-toggle.btn-info { @@ -3203,9 +3324,17 @@ fieldset[disabled] .btn-info.active { background-color: #f0ad4e; border-color: #eea236; } -.btn-warning:hover, .btn-warning:focus, -.btn-warning.focus, +.btn-warning.focus { + color: #fff; + background-color: #ec971f; + border-color: #985f0d; +} +.btn-warning:hover { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { @@ -3213,6 +3342,19 @@ fieldset[disabled] .btn-info.active { background-color: #ec971f; border-color: #d58512; } +.btn-warning:active:hover, +.btn-warning.active:hover, +.open > .dropdown-toggle.btn-warning:hover, +.btn-warning:active:focus, +.btn-warning.active:focus, +.open > .dropdown-toggle.btn-warning:focus, +.btn-warning:active.focus, +.btn-warning.active.focus, +.open > .dropdown-toggle.btn-warning.focus { + color: #fff; + background-color: #d58512; + border-color: #985f0d; +} .btn-warning:active, .btn-warning.active, .open > .dropdown-toggle.btn-warning { @@ -3248,9 +3390,17 @@ fieldset[disabled] .btn-warning.active { background-color: #d9534f; border-color: #d43f3a; } -.btn-danger:hover, .btn-danger:focus, -.btn-danger.focus, +.btn-danger.focus { + color: #fff; + background-color: #c9302c; + border-color: #761c19; +} +.btn-danger:hover { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { @@ -3258,6 +3408,19 @@ fieldset[disabled] .btn-warning.active { background-color: #c9302c; border-color: #ac2925; } +.btn-danger:active:hover, +.btn-danger.active:hover, +.open > .dropdown-toggle.btn-danger:hover, +.btn-danger:active:focus, +.btn-danger.active:focus, +.open > .dropdown-toggle.btn-danger:focus, +.btn-danger:active.focus, +.btn-danger.active.focus, +.open > .dropdown-toggle.btn-danger.focus { + color: #fff; + background-color: #ac2925; + border-color: #761c19; +} .btn-danger:active, .btn-danger.active, .open > .dropdown-toggle.btn-danger { @@ -3365,11 +3528,9 @@ input[type="button"].btn-block { } .collapse { display: none; - visibility: hidden; } .collapse.in { display: block; - visibility: visible; } tr.collapse.in { display: table-row; @@ -3397,7 +3558,8 @@ tbody.collapse.in { height: 0; margin-left: 2px; vertical-align: middle; - border-top: 4px solid; + border-top: 4px dashed; + border-top: 4px solid \9; border-right: 4px solid transparent; border-left: 4px solid transparent; } @@ -3514,7 +3676,8 @@ tbody.collapse.in { .navbar-fixed-bottom .dropdown .caret { content: ""; border-top: 0; - border-bottom: 4px solid; + border-bottom: 4px dashed; + border-bottom: 4px solid \9; } .dropup .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu { @@ -3562,6 +3725,7 @@ tbody.collapse.in { .btn-toolbar { margin-left: -5px; } +.btn-toolbar .btn, .btn-toolbar .btn-group, .btn-toolbar .input-group { float: left; @@ -3852,6 +4016,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group { + z-index: 2; margin-left: -1px; } .nav { @@ -4037,11 +4202,9 @@ select[multiple].input-group-sm > .input-group-btn > .btn { } .tab-content > .tab-pane { display: none; - visibility: hidden; } .tab-content > .active { display: block; - visibility: visible; } .nav-tabs .dropdown-menu { margin-top: -1px; @@ -4088,7 +4251,6 @@ select[multiple].input-group-sm > .input-group-btn > .btn { height: auto !important; padding-bottom: 0; overflow: visible !important; - visibility: visible !important; } .navbar-collapse.in { overflow-y: visible; @@ -4630,6 +4792,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination > li > span:hover, .pagination > li > a:focus, .pagination > li > span:focus { + z-index: 3; color: #23527c; background-color: #eee; border-color: #ddd; @@ -4661,6 +4824,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination-lg > li > span { padding: 10px 16px; font-size: 18px; + line-height: 1.3333333; } .pagination-lg > li:first-child > a, .pagination-lg > li:first-child > span { @@ -4676,6 +4840,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination-sm > li > span { padding: 5px 10px; font-size: 12px; + line-height: 1.5; } .pagination-sm > li:first-child > a, .pagination-sm > li:first-child > span { @@ -4802,7 +4967,7 @@ a.label:focus { color: #fff; text-align: center; white-space: nowrap; - vertical-align: baseline; + vertical-align: middle; background-color: #777; border-radius: 10px; } @@ -4813,7 +4978,8 @@ a.label:focus { position: relative; top: -1px; } -.btn-xs .badge { +.btn-xs .badge, +.btn-group-xs > .btn .badge { top: 0; padding: 1px 5px; } @@ -4838,7 +5004,8 @@ a.badge:focus { margin-left: 3px; } .jumbotron { - padding: 30px 15px; + padding-top: 30px; + padding-bottom: 30px; margin-bottom: 30px; color: inherit; background-color: #eee; @@ -4864,7 +5031,8 @@ a.badge:focus { } @media screen and (min-width: 768px) { .jumbotron { - padding: 48px 0; + padding-top: 48px; + padding-bottom: 48px; } .container .jumbotron, .container-fluid .jumbotron { @@ -5088,6 +5256,9 @@ a.thumbnail.active { .media-object { display: block; } +.media-object.img-thumbnail { + max-width: none; +} .media-right, .media > .pull-right { padding-left: 10px; @@ -5137,18 +5308,26 @@ a.thumbnail.active { border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; } -a.list-group-item { +a.list-group-item, +button.list-group-item { color: #555; } -a.list-group-item .list-group-item-heading { +a.list-group-item .list-group-item-heading, +button.list-group-item .list-group-item-heading { color: #333; } a.list-group-item:hover, -a.list-group-item:focus { +button.list-group-item:hover, +a.list-group-item:focus, +button.list-group-item:focus { color: #555; text-decoration: none; background-color: #f5f5f5; } +button.list-group-item { + width: 100%; + text-align: left; +} .list-group-item.disabled, .list-group-item.disabled:hover, .list-group-item.disabled:focus { @@ -5194,20 +5373,27 @@ a.list-group-item:focus { color: #3c763d; background-color: #dff0d8; } -a.list-group-item-success { +a.list-group-item-success, +button.list-group-item-success { color: #3c763d; } -a.list-group-item-success .list-group-item-heading { +a.list-group-item-success .list-group-item-heading, +button.list-group-item-success .list-group-item-heading { color: inherit; } a.list-group-item-success:hover, -a.list-group-item-success:focus { +button.list-group-item-success:hover, +a.list-group-item-success:focus, +button.list-group-item-success:focus { color: #3c763d; background-color: #d0e9c6; } a.list-group-item-success.active, +button.list-group-item-success.active, a.list-group-item-success.active:hover, -a.list-group-item-success.active:focus { +button.list-group-item-success.active:hover, +a.list-group-item-success.active:focus, +button.list-group-item-success.active:focus { color: #fff; background-color: #3c763d; border-color: #3c763d; @@ -5216,20 +5402,27 @@ a.list-group-item-success.active:focus { color: #31708f; background-color: #d9edf7; } -a.list-group-item-info { +a.list-group-item-info, +button.list-group-item-info { color: #31708f; } -a.list-group-item-info .list-group-item-heading { +a.list-group-item-info .list-group-item-heading, +button.list-group-item-info .list-group-item-heading { color: inherit; } a.list-group-item-info:hover, -a.list-group-item-info:focus { +button.list-group-item-info:hover, +a.list-group-item-info:focus, +button.list-group-item-info:focus { color: #31708f; background-color: #c4e3f3; } a.list-group-item-info.active, +button.list-group-item-info.active, a.list-group-item-info.active:hover, -a.list-group-item-info.active:focus { +button.list-group-item-info.active:hover, +a.list-group-item-info.active:focus, +button.list-group-item-info.active:focus { color: #fff; background-color: #31708f; border-color: #31708f; @@ -5238,20 +5431,27 @@ a.list-group-item-info.active:focus { color: #8a6d3b; background-color: #fcf8e3; } -a.list-group-item-warning { +a.list-group-item-warning, +button.list-group-item-warning { color: #8a6d3b; } -a.list-group-item-warning .list-group-item-heading { +a.list-group-item-warning .list-group-item-heading, +button.list-group-item-warning .list-group-item-heading { color: inherit; } a.list-group-item-warning:hover, -a.list-group-item-warning:focus { +button.list-group-item-warning:hover, +a.list-group-item-warning:focus, +button.list-group-item-warning:focus { color: #8a6d3b; background-color: #faf2cc; } a.list-group-item-warning.active, +button.list-group-item-warning.active, a.list-group-item-warning.active:hover, -a.list-group-item-warning.active:focus { +button.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus, +button.list-group-item-warning.active:focus { color: #fff; background-color: #8a6d3b; border-color: #8a6d3b; @@ -5260,20 +5460,27 @@ a.list-group-item-warning.active:focus { color: #a94442; background-color: #f2dede; } -a.list-group-item-danger { +a.list-group-item-danger, +button.list-group-item-danger { color: #a94442; } -a.list-group-item-danger .list-group-item-heading { +a.list-group-item-danger .list-group-item-heading, +button.list-group-item-danger .list-group-item-heading { color: inherit; } a.list-group-item-danger:hover, -a.list-group-item-danger:focus { +button.list-group-item-danger:hover, +a.list-group-item-danger:focus, +button.list-group-item-danger:focus { color: #a94442; background-color: #ebcccc; } a.list-group-item-danger.active, +button.list-group-item-danger.active, a.list-group-item-danger.active:hover, -a.list-group-item-danger.active:focus { +button.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus, +button.list-group-item-danger.active:focus { color: #fff; background-color: #a94442; border-color: #a94442; @@ -5347,6 +5554,10 @@ a.list-group-item-danger.active:focus { border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; } +.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} .panel-heading + .list-group .list-group-item:first-child { border-top-width: 0; } @@ -5645,10 +5856,10 @@ a.list-group-item-danger.active:focus { height: 100%; border: 0; } -.embed-responsive.embed-responsive-16by9 { +.embed-responsive-16by9 { padding-bottom: 56.25%; } -.embed-responsive.embed-responsive-4by3 { +.embed-responsive-4by3 { padding-bottom: 75%; } .well { @@ -5707,7 +5918,7 @@ button.close { right: 0; bottom: 0; left: 0; - z-index: 1040; + z-index: 1050; display: none; overflow: hidden; -webkit-overflow-scrolling: touch; @@ -5750,10 +5961,12 @@ button.close { box-shadow: 0 3px 9px rgba(0, 0, 0, .5); } .modal-backdrop { - position: absolute; + position: fixed; top: 0; right: 0; + bottom: 0; left: 0; + z-index: 1040; background-color: #000; } .modal-backdrop.fade { @@ -5826,11 +6039,23 @@ button.close { display: block; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 12px; + font-style: normal; font-weight: normal; - line-height: 1.4; - visibility: visible; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; filter: alpha(opacity=0); opacity: 0; + + line-break: auto; } .tooltip.in { filter: alpha(opacity=90); @@ -5857,7 +6082,6 @@ button.close { padding: 3px 8px; color: #fff; text-align: center; - text-decoration: none; background-color: #000; border-radius: 4px; } @@ -5934,9 +6158,18 @@ button.close { padding: 1px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; + font-style: normal; font-weight: normal; line-height: 1.42857143; text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; white-space: normal; background-color: #fff; -webkit-background-clip: padding-box; @@ -5946,6 +6179,8 @@ button.close { border-radius: 6px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + + line-break: auto; } .popover.top { margin-top: -10px; @@ -6073,8 +6308,8 @@ button.close { -webkit-backface-visibility: hidden; backface-visibility: hidden; - -webkit-perspective: 1000; - perspective: 1000; + -webkit-perspective: 1000px; + perspective: 1000px; } .carousel-inner > .item.next, .carousel-inner > .item.active.right { @@ -6173,6 +6408,7 @@ button.close { top: 50%; z-index: 5; display: inline-block; + margin-top: -10px; } .carousel-control .icon-prev, .carousel-control .glyphicon-chevron-left { @@ -6188,7 +6424,6 @@ button.close { .carousel-control .icon-next { width: 20px; height: 20px; - margin-top: -10px; font-family: serif; line-height: 1; } @@ -6348,7 +6583,6 @@ button.close { } .hidden { display: none !important; - visibility: hidden !important; } .affix { position: fixed; @@ -6381,7 +6615,7 @@ button.close { display: block !important; } table.visible-xs { - display: table; + display: table !important; } tr.visible-xs { display: table-row !important; @@ -6411,7 +6645,7 @@ button.close { display: block !important; } table.visible-sm { - display: table; + display: table !important; } tr.visible-sm { display: table-row !important; @@ -6441,7 +6675,7 @@ button.close { display: block !important; } table.visible-md { - display: table; + display: table !important; } tr.visible-md { display: table-row !important; @@ -6471,7 +6705,7 @@ button.close { display: block !important; } table.visible-lg { - display: table; + display: table !important; } tr.visible-lg { display: table-row !important; @@ -6524,7 +6758,7 @@ button.close { display: block !important; } table.visible-print { - display: table; + display: table !important; } tr.visible-print { display: table-row !important; diff --git a/examples/Tornado-example/static/js/Comment.jsx b/examples/Tornado-example/static/js/Comment.jsx new file mode 100644 index 0000000..f173afd --- /dev/null +++ b/examples/Tornado-example/static/js/Comment.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +class Comment extends React.Component { + render() { + return ( +
+

{this.props.author}

+ {this.props.text} +
+ ); + } +} + +export default Comment; diff --git a/examples/Tornado-example/static/js/CommentBox.jsx b/examples/Tornado-example/static/js/CommentBox.jsx new file mode 100644 index 0000000..4d645ed --- /dev/null +++ b/examples/Tornado-example/static/js/CommentBox.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import CommentList from './CommentList.jsx'; +import CommentForm from './CommentForm.jsx'; + +class CommentBox extends React.Component { + render() { + return ( +
+ + +
+ ); + } +} + +export default CommentBox; diff --git a/examples/Tornado-example/static/js/CommentForm.jsx b/examples/Tornado-example/static/js/CommentForm.jsx new file mode 100644 index 0000000..137d09f --- /dev/null +++ b/examples/Tornado-example/static/js/CommentForm.jsx @@ -0,0 +1,30 @@ +import React from 'react'; + +class CommentForm extends React.Component { + render() { + return ( +
+ +

Submit a comment

+
+ +
+
+