From d89f7c5c55f0496a51441b6d16103ac50c0bbfe6 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sun, 7 Feb 2016 16:38:25 +0000 Subject: [PATCH 01/11] Added isomorphism, swig views, webpack configuration and separated JSX templates into separate file --- .gitignore | 2 + README.md | 64 +++++---------- package.json | 24 ++++-- public/scripts/.gitignore | 2 + public/scripts/example.js | 146 ---------------------------------- requirements.txt | 1 - server.go | 112 -------------------------- server.js | 30 ++++++- server.php | 53 ------------- server.pl | 38 --------- server.py | 36 --------- server.rb | 49 ------------ src/example.js | 147 +++++++++++++++++++++++++++++++++++ src/index.js | 1 + src/templates.jsx | 57 ++++++++++++++ {public => views}/index.html | 23 +++--- webpack.config.js | 34 ++++++++ 17 files changed, 323 insertions(+), 496 deletions(-) create mode 100644 public/scripts/.gitignore delete mode 100644 public/scripts/example.js delete mode 100644 requirements.txt delete mode 100644 server.go delete mode 100644 server.php delete mode 100644 server.pl delete mode 100644 server.py delete mode 100644 server.rb create mode 100644 src/example.js create mode 100644 src/index.js create mode 100644 src/templates.jsx rename {public => views}/index.html (50%) create mode 100644 webpack.config.js diff --git a/.gitignore b/.gitignore index daeba5f9..10075f46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *~ node_modules .DS_Store +.idea +npm-debug.log \ No newline at end of file diff --git a/README.md b/README.md index 4862f5df..f62c1983 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,33 @@ -[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) +# Isomorphic React and Express -# React Tutorial +This repository shows how React can be used with the Express framework isomorphically; that is to use the same code on both the server and browser. This uses the comment box example from the [React tutorial](http://facebook.github.io/react/docs/tutorial.html) but in addition to rendering the comment box in the browser, it pre-renders it on the server, using the same code. -This is the React comment box example from [the React tutorial](http://facebook.github.io/react/docs/tutorial.html). +There are also a few other additions. I've set up `webpack` so that it bundles up the code to be used in the browser. I also didn't particularly like the `JSX` template render methods being in the same file as the controller code (despite what React says, they are controllers; they behave exactly the same way as Angular's controllers). And lastly since I had to server-side rendering, I've had to use a view engine. I've chose `Swig` as unlike `Jade` it uses proper HTML and so means I have one fewer language to learn (which fits into the philosophy of isomorphism). -## To use - -There are several simple server implementations included. They all serve static files from `public/` and handle requests to `/api/comments` to fetch or add data. Start a server with one of the following: - -### Node - -```sh -npm install -node server.js -``` +Naturally this means all of the other server language implementations have been removed - Python, PHP etc. -### Python +## Implementation -```sh -pip install -r requirements.txt -python server.py -``` +The example file from the tutorial is now in a publicly-inaccessible location at `src/example.js`. The templates are in another file which is `require`d from there, `templates.jsx`. -### Ruby -```sh -ruby server.rb -``` +In the browser, the main entry point method which calls `ReactDOM.render`, is exported as `render` from `example.js`. This is made accessible as a window method in `src/index.js`, which is used as the entry script for `webpack`. This is all compiled into `public/scripts/bundle.js` which is what the browser includes. The settings for the `webpack` are found in `webpack.config.js`; several libraries like React itself and the markdown parser are set to be externals, which means they are not bundled up so that the browser can load those from a CDN (Content Delivery Network). -### PHP -```sh -php server.php -``` +On the browse, `example.js` itself is `require`d, after the `node-jsx` module is set up, in order that the JSX syntax can be understood. `example.js` also exports a `renderServer` which returns a static string after calling `ReactDOMServer.renderToString` from the `react-dom/server` module. The route for `/` simply calls this method and passes it as a variable to the `views/index.html` view. -### Go -```sh -go run server.go -``` - -### Perl - -```sh -cpan Mojolicious -perl server.pl -``` +## To use -And visit . Try opening multiple tabs! + npm install + +Next, to compile the browser code into `public/scripts/bundle.js`: -## Changing the port + npm run webpack-dev + +Or if you want to compile it to a minified version without the source map: -You can change the port number by setting the `$PORT` environment variable before invoking any of the scripts above, e.g., + npm run webpack + +And then start the server: -```sh -PORT=3001 node server.js -``` + npm start + +And visit . \ No newline at end of file diff --git a/package.json b/package.json index e7491981..c9f367b5 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,26 @@ "main": "server.js", "dependencies": { "body-parser": "^1.4.3", - "express": "^4.4.5" + "express": "^4.4.5", + "marked": "^0.3.5", + "node-jsx": "^0.13.3", + "react": "^0.14.7", + "react-dom": "^0.14.7", + "swig": "^1.4.2" + }, + "devDependencies": { + "babel-core": "^6.4.5", + "babel-loader": "^6.2.2", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "babelify": "^7.2.0", + "webpack": "^1.12.13" }, - "devDependencies": {}, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js" + "start": "node server.js", + "webpack-dev": "webpack --watch --dev", + "webpack": "webpack" }, "repository": { "type": "git", @@ -27,7 +41,7 @@ "url": "https://github.com/reactjs/react-tutorial/issues" }, "homepage": "https://github.com/reactjs/react-tutorial", - "engines" : { - "node" : "0.12.x" + "engines": { + "node": "0.12.x" } } diff --git a/public/scripts/.gitignore b/public/scripts/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/public/scripts/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/public/scripts/example.js b/public/scripts/example.js deleted file mode 100644 index c249427a..00000000 --- a/public/scripts/example.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * This file provided by Facebook is for non-commercial testing and evaluation - * purposes only. Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN - * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -var Comment = React.createClass({ - rawMarkup: function() { - var rawMarkup = marked(this.props.children.toString(), {sanitize: true}); - return { __html: rawMarkup }; - }, - - render: function() { - return ( -
-

- {this.props.author} -

- -
- ); - } -}); - -var CommentBox = React.createClass({ - loadCommentsFromServer: function() { - $.ajax({ - url: this.props.url, - dataType: 'json', - cache: false, - success: function(data) { - this.setState({data: data}); - }.bind(this), - error: function(xhr, status, err) { - console.error(this.props.url, status, err.toString()); - }.bind(this) - }); - }, - handleCommentSubmit: function(comment) { - var comments = this.state.data; - // Optimistically set an id on the new comment. It will be replaced by an - // id generated by the server. In a production application you would likely - // not use Date.now() for this and would have a more robust system in place. - comment.id = Date.now(); - var newComments = comments.concat([comment]); - this.setState({data: newComments}); - $.ajax({ - url: this.props.url, - dataType: 'json', - type: 'POST', - data: comment, - success: function(data) { - this.setState({data: data}); - }.bind(this), - error: function(xhr, status, err) { - this.setState({data: comments}); - console.error(this.props.url, status, err.toString()); - }.bind(this) - }); - }, - getInitialState: function() { - return {data: []}; - }, - componentDidMount: function() { - this.loadCommentsFromServer(); - setInterval(this.loadCommentsFromServer, this.props.pollInterval); - }, - render: function() { - return ( -
-

Comments

- - -
- ); - } -}); - -var CommentList = React.createClass({ - render: function() { - var commentNodes = this.props.data.map(function(comment) { - return ( - - {comment.text} - - ); - }); - return ( -
- {commentNodes} -
- ); - } -}); - -var CommentForm = React.createClass({ - getInitialState: function() { - return {author: '', text: ''}; - }, - handleAuthorChange: function(e) { - this.setState({author: e.target.value}); - }, - handleTextChange: function(e) { - this.setState({text: e.target.value}); - }, - handleSubmit: function(e) { - e.preventDefault(); - var author = this.state.author.trim(); - var text = this.state.text.trim(); - if (!text || !author) { - return; - } - this.props.onCommentSubmit({author: author, text: text}); - this.setState({author: '', text: ''}); - }, - render: function() { - return ( -
- - - -
- ); - } -}); - -ReactDOM.render( - , - document.getElementById('content') -); diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 632a1efa..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -Flask==0.10.1 diff --git a/server.go b/server.go deleted file mode 100644 index 934a4cfc..00000000 --- a/server.go +++ /dev/null @@ -1,112 +0,0 @@ -/** - * This file provided by Facebook is for non-commercial testing and evaluation - * purposes only. Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN - * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "os" - "sync" - "time" -) - -type comment struct { - ID int64 `json:"id"` - Author string `json:"author"` - Text string `json:"text"` -} - -const dataFile = "./comments.json" - -var commentMutex = new(sync.Mutex) - -// Handle comments -func handleComments(w http.ResponseWriter, r *http.Request) { - // Since multiple requests could come in at once, ensure we have a lock - // around all file operations - commentMutex.Lock() - defer commentMutex.Unlock() - - // Stat the file, so we can find its current permissions - fi, err := os.Stat(dataFile) - if err != nil { - http.Error(w, fmt.Sprintf("Unable to stat the data file (%s): %s", dataFile, err), http.StatusInternalServerError) - return - } - - // Read the comments from the file. - commentData, err := ioutil.ReadFile(dataFile) - if err != nil { - http.Error(w, fmt.Sprintf("Unable to read the data file (%s): %s", dataFile, err), http.StatusInternalServerError) - return - } - - switch r.Method { - case "POST": - // Decode the JSON data - var comments []comment - if err := json.Unmarshal(commentData, &comments); err != nil { - http.Error(w, fmt.Sprintf("Unable to Unmarshal comments from data file (%s): %s", dataFile, err), http.StatusInternalServerError) - return - } - - // Add a new comment to the in memory slice of comments - comments = append(comments, comment{ID: time.Now().UnixNano() / 1000000, Author: r.FormValue("author"), Text: r.FormValue("text")}) - - // Marshal the comments to indented json. - commentData, err = json.MarshalIndent(comments, "", " ") - if err != nil { - http.Error(w, fmt.Sprintf("Unable to marshal comments to json: %s", err), http.StatusInternalServerError) - return - } - - // Write out the comments to the file, preserving permissions - err := ioutil.WriteFile(dataFile, commentData, fi.Mode()) - if err != nil { - http.Error(w, fmt.Sprintf("Unable to write comments to data file (%s): %s", dataFile, err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Access-Control-Allow-Origin", "*") - io.Copy(w, bytes.NewReader(commentData)) - - case "GET": - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Access-Control-Allow-Origin", "*") - // stream the contents of the file to the response - io.Copy(w, bytes.NewReader(commentData)) - - default: - // Don't know the method, so error - http.Error(w, fmt.Sprintf("Unsupported method: %s", r.Method), http.StatusMethodNotAllowed) - } -} - -func main() { - port := os.Getenv("PORT") - if port == "" { - port = "3000" - } - http.HandleFunc("/api/comments", handleComments) - http.Handle("/", http.FileServer(http.Dir("./public"))) - log.Println("Server started: http://localhost:" + port) - log.Fatal(http.ListenAndServe(":"+port, nil)) -} diff --git a/server.js b/server.js index b5a7218a..d0097c10 100644 --- a/server.js +++ b/server.js @@ -14,19 +14,43 @@ var fs = require('fs'); var path = require('path'); var express = require('express'); var bodyParser = require('body-parser'); +var swig = require('swig'); + +require('node-jsx').install(); +var example = require('./src/example'); + var app = express(); +app.engine('html', swig.renderFile); +app.set('view engine', 'html'); +app.set('views', __dirname + '/views'); +app.set('view cache', false); + var COMMENTS_FILE = path.join(__dirname, 'comments.json'); app.set('port', (process.env.PORT || 3000)); -app.use('/', express.static(path.join(__dirname, 'public'))); +app.use(express.static(path.join(__dirname, 'public'))); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: true})); -// Additional middleware which will set headers that we need on each request. + +app.get('/', function(req, res) { + fs.readFile(COMMENTS_FILE, function(err, data) { + if (err) { + console.error(err); + process.exit(1); + } + + res.render('index', { + commentBox: example.renderServer(JSON.parse(data)) + }); + }); +}); + +// Additional middleware which will set headers that we need on each /api request. app.use(function(req, res, next) { - // Set permissive CORS header - this allows this server to be used only as + // Set permissive CORS header - this allows the api routes to be used only as // an API server in conjunction with something like webpack-dev-server. res.setHeader('Access-Control-Allow-Origin', '*'); diff --git a/server.php b/server.php deleted file mode 100644 index 75fae215..00000000 --- a/server.php +++ /dev/null @@ -1,53 +0,0 @@ - round(microtime(true) * 1000), - 'author' => $_POST['author'], - 'text' => $_POST['text'] - ]; - - $comments = json_encode($commentsDecoded, JSON_PRETTY_PRINT); - file_put_contents('comments.json', $comments); - } - header('Content-Type: application/json'); - header('Cache-Control: no-cache'); - header('Access-Control-Allow-Origin: *'); - echo $comments; - } else { - return false; - } -} diff --git a/server.pl b/server.pl deleted file mode 100644 index c3212b9c..00000000 --- a/server.pl +++ /dev/null @@ -1,38 +0,0 @@ -# This file provided by Facebook is for non-commercial testing and evaluation -# purposes only. Facebook reserves all rights not expressly granted. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -use Time::HiRes qw(gettimeofday); -use Mojolicious::Lite; -use Mojo::JSON qw(encode_json decode_json); - -app->static->paths->[0] = './public'; - -any '/' => sub { $_[0]->reply->static('index.html') }; - -any [qw(GET POST)] => '/api/comments' => sub { - my $self = shift; - my $comments = decode_json (do { local(@ARGV,$/) = 'comments.json';<> }); - $self->res->headers->cache_control('no-cache'); - $self->res->headers->access_control_allow_origin('*'); - - if ($self->req->method eq 'POST') - { - push @$comments, { - id => int(gettimeofday * 1000), - author => $self->param('author'), - text => $self->param('text'), - }; - open my $FILE, '>', 'comments.json'; - print $FILE encode_json($comments); - } - $self->render(json => $comments); -}; -my $port = $ENV{PORT} || 3000; -app->start('daemon', '-l', "http://*:$port"); diff --git a/server.py b/server.py deleted file mode 100644 index 5cf598df..00000000 --- a/server.py +++ /dev/null @@ -1,36 +0,0 @@ -# This file provided by Facebook is for non-commercial testing and evaluation -# purposes only. Facebook reserves all rights not expressly granted. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import json -import os -import time -from flask import Flask, Response, request - -app = Flask(__name__, static_url_path='', static_folder='public') -app.add_url_rule('/', 'root', lambda: app.send_static_file('index.html')) - -@app.route('/api/comments', methods=['GET', 'POST']) -def comments_handler(): - - with open('comments.json', 'r') as file: - comments = json.loads(file.read()) - - if request.method == 'POST': - newComment = request.form.to_dict() - newComment['id'] = int(time.time() * 1000) - comments.append(newComment) - - with open('comments.json', 'w') as file: - file.write(json.dumps(comments, indent=4, separators=(',', ': '))) - - return Response(json.dumps(comments), mimetype='application/json', headers={'Cache-Control': 'no-cache', 'Access-Control-Allow-Origin': '*'}) - -if __name__ == '__main__': - app.run(port=int(os.environ.get("PORT",3000))) diff --git a/server.rb b/server.rb deleted file mode 100644 index 698f4339..00000000 --- a/server.rb +++ /dev/null @@ -1,49 +0,0 @@ -# This file provided by Facebook is for non-commercial testing and evaluation -# purposes only. Facebook reserves all rights not expressly granted. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -require 'webrick' -require 'json' - -# default port to 3000 or overwrite with PORT variable by running -# $ PORT=3001 ruby server.rb -port = ENV['PORT'] ? ENV['PORT'].to_i : 3000 - -puts "Server started: http://localhost:#{port}/" - -root = File.expand_path './public' -server = WEBrick::HTTPServer.new Port: port, DocumentRoot: root - -server.mount_proc '/api/comments' do |req, res| - comments = JSON.parse(File.read('./comments.json', encoding: 'UTF-8')) - - if req.request_method == 'POST' - # Assume it's well formed - comment = { id: (Time.now.to_f * 1000).to_i } - req.query.each do |key, value| - comment[key] = value.force_encoding('UTF-8') unless key == 'id' - end - comments << comment - File.write( - './comments.json', - JSON.pretty_generate(comments, indent: ' '), - encoding: 'UTF-8' - ) - end - - # always return json - res['Content-Type'] = 'application/json' - res['Cache-Control'] = 'no-cache' - res['Access-Control-Allow-Origin'] = '*' - res.body = JSON.generate(comments) -end - -trap('INT') { server.shutdown } - -server.start diff --git a/src/example.js b/src/example.js new file mode 100644 index 00000000..15e6cb2d --- /dev/null +++ b/src/example.js @@ -0,0 +1,147 @@ +/** + * This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +var templates = require('./templates.jsx'); +var React = require('react'); +var marked = require('marked'); + +var ReactDOMServer = typeof window === "undefined" ? require('react-dom/server') : undefined; + +var Comment = React.createClass({ + displayName: 'Comment', + rawMarkup: function () { + var rawMarkup = marked(this.props.children.toString(), {sanitize: true}); + return {__html: rawMarkup}; + }, + + render: templates.comment +}); + +var CommentBox = React.createClass({ + displayName: 'CommentBox', + loadCommentsFromServer: function () { + $.get(this.props.url).then( + (data) => { + this.setState({data: data}); + }, + (err) => { + console.error(this.props.url, err.status, err.statusText); + } + ); + }, + handleCommentSubmit: function (comment) { + var comments = this.state.data; + // Optimistically set an id on the new comment. It will be replaced by an + // id generated by the server. In a production application you would likely + // not use Date.now() for this and would have a more robust system in place. + comment.id = Date.now(); + var newComments = comments.concat([comment]); + this.setState({data: newComments}); + + $.ajax({ + url: this.props.url, + dataType: 'json', + type: 'POST', + data: comment, + success: (data) => { + this.setState({data: data}); + }, + error: function (xhr, status, err) { + this.setState({data: comments}); + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + getInitialState: function () { + return {data: this.props.comments}; + }, + componentDidMount: function () { + this.loadCommentsFromServer(); + setInterval(this.loadCommentsFromServer, this.props.pollInterval); + }, + render: function () { + return templates.commentBox.apply(this, [CommentList, CommentForm]); + } +}); + +var CommentList = React.createClass({ + displayName: 'CommentList', + render: function () { + return templates.commentList.apply(this, [Comment]); + } +}); + +var CommentForm = React.createClass({ + displayName: 'CommentForm', + getInitialState: function () { + return {author: '', text: ''}; + }, + handleAuthorChange: function (e) { + this.setState({author: e.target.value}); + }, + handleTextChange: function (e) { + this.setState({text: e.target.value}); + }, + handleSubmit: function (e) { + e.preventDefault(); + var author = this.state.author.trim(); + var text = this.state.text.trim(); + if (!text || !author) { + return; + } + this.props.onCommentSubmit({author: author, text: text}); + this.setState({author: '', text: ''}); + }, + render: templates.commentForm +}); + + +module.exports = { + /** + * Load the comments via AJAX before rendering the comment box with the DOM. + * This will avoid the server rendered comments being replaced with nothing by JS. + * If the AJAX call fails, then just render no comments after logging the error. + */ + render: () => { + var url = "/api/comments"; + + $.get(url).then( + (comments) => { + return comments; + }, + (err) => { + console.error(this.props.url, err.status, err.statusText); + return []; + } + ) + .always((comments) => { + ReactDOM.render(React.createElement(CommentBox, { + url: url, + pollInterval: 2000, + comments: comments + }), document.getElementById('content')); + }); + }, + /** + * Just return a static string to render on the server + * + * @param {Array} comments + * @returns {String} + */ + renderServer: (comments) => { + return ReactDOMServer.renderToString(React.createElement(CommentBox, { + url: "/api/comments", + pollInterval: 2000, + comments: comments + })); + } +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..c4a1da58 --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +window.commentBoxRender = require('./example.js').render; \ No newline at end of file diff --git a/src/templates.jsx b/src/templates.jsx new file mode 100644 index 00000000..f71096b8 --- /dev/null +++ b/src/templates.jsx @@ -0,0 +1,57 @@ +var React = require('react'); + +module.exports = { + comment: function () { + return ( +
+

+ {this.props.author} +

+ +
+ ); + }, + commentBox: function (CommentList, CommentForm) { + return ( +
+

Comments

+ + +
+ ); + }, + commentList: function (Comment) { + var commentNodes = this.props.data.map(function(comment) { + return ( + + {comment.text} + + ); + }); + + return ( +
+ {commentNodes} +
+ ); + }, + commentForm: function () { + return ( +
+ + + +
+ ); + } +}; diff --git a/public/index.html b/views/index.html similarity index 50% rename from public/index.html rename to views/index.html index c6494446..54e8c10d 100644 --- a/public/index.html +++ b/views/index.html @@ -4,19 +4,22 @@ React Tutorial - - - - + + + + -
- - +

Thingy

+ +
+ {% autoescape false %} + {{ commentBox }} + {% endautoescape %} +
+ + diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..d5c9900c --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,34 @@ +var webpack = require("webpack"); +var isDev = process.argv.indexOf('--dev') > -1; + +// Don't include react-dom/server in the browser +var ignore = new webpack.IgnorePlugin(/react-dom/); + +module.exports = { + entry: "./src/index.js", + output: { + path: "./public/scripts", + filename: "bundle.js" + }, + // These libraries are included in separate script tags and are available as global variables + externals: { + "react": "React", + "marked": "marked" + }, + plugins: isDev ? + [ignore] : + [ignore, new webpack.optimize.UglifyJsPlugin({minimize: true})], + devtool: isDev ? 'source-map' : null, + module: { + loaders: [ + { + test: /\.jsx?$/, + exclude: /(node_modules|bower_components)/, + loader: 'babel', // 'babel-loader' is also a legal name to reference + query: { + presets: ['react', 'es2015'] + } + } + ] + } +}; \ No newline at end of file From 38e59a31c85836070893e12eaa5a831f232e2e30 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sun, 7 Feb 2016 16:55:02 +0000 Subject: [PATCH 02/11] Removed stupid h1 --- views/index.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/views/index.html b/views/index.html index 54e8c10d..dda3f7ed 100644 --- a/views/index.html +++ b/views/index.html @@ -12,14 +12,11 @@ -

Thingy

-
{% autoescape false %} {{ commentBox }} {% endautoescape %}
- From 39c673814bb9e3bb4845dba76c9ae9b35f2c2709 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sun, 7 Feb 2016 16:56:05 +0000 Subject: [PATCH 03/11] Corrected spelling mistaek --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f62c1983..09047b7d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The example file from the tutorial is now in a publicly-inaccessible location at In the browser, the main entry point method which calls `ReactDOM.render`, is exported as `render` from `example.js`. This is made accessible as a window method in `src/index.js`, which is used as the entry script for `webpack`. This is all compiled into `public/scripts/bundle.js` which is what the browser includes. The settings for the `webpack` are found in `webpack.config.js`; several libraries like React itself and the markdown parser are set to be externals, which means they are not bundled up so that the browser can load those from a CDN (Content Delivery Network). -On the browse, `example.js` itself is `require`d, after the `node-jsx` module is set up, in order that the JSX syntax can be understood. `example.js` also exports a `renderServer` which returns a static string after calling `ReactDOMServer.renderToString` from the `react-dom/server` module. The route for `/` simply calls this method and passes it as a variable to the `views/index.html` view. +On the browser, `example.js` itself is `require`d, after the `node-jsx` module is set up, in order that the JSX syntax can be understood. `example.js` also exports a `renderServer` which returns a static string after calling `ReactDOMServer.renderToString` from the `react-dom/server` module. The route for `/` simply calls this method and passes it as a variable to the `views/index.html` view. ## To use From f88293b932e7d51f7d92a1991e33ba99e3dd4352 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Tue, 9 Feb 2016 01:47:40 +0000 Subject: [PATCH 04/11] Refactored slightly so that the components file only exports the components and the server and browser specific render methods are in their own files --- README.md | 6 +-- components/browser.js | 28 +++++++++++++ src/example.js => components/components.js | 48 +++------------------- components/server.js | 15 +++++++ {src => components}/templates.jsx | 0 package.json | 1 + server.js | 6 ++- src/index.js | 1 - views/index.html | 2 +- webpack.config.js | 13 +++--- 10 files changed, 64 insertions(+), 56 deletions(-) create mode 100644 components/browser.js rename src/example.js => components/components.js (71%) create mode 100644 components/server.js rename {src => components}/templates.jsx (100%) delete mode 100644 src/index.js diff --git a/README.md b/README.md index 09047b7d..900c240f 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ Naturally this means all of the other server language implementations have been ## Implementation -The example file from the tutorial is now in a publicly-inaccessible location at `src/example.js`. The templates are in another file which is `require`d from there, `templates.jsx`. +The example file from the tutorial is now in a publicly-inaccessible location at `components/components.js`. This exports each component in `module.exports`. The templates are in another file which is `require`d from there, `templates.jsx`. -In the browser, the main entry point method which calls `ReactDOM.render`, is exported as `render` from `example.js`. This is made accessible as a window method in `src/index.js`, which is used as the entry script for `webpack`. This is all compiled into `public/scripts/bundle.js` which is what the browser includes. The settings for the `webpack` are found in `webpack.config.js`; several libraries like React itself and the markdown parser are set to be externals, which means they are not bundled up so that the browser can load those from a CDN (Content Delivery Network). +In the browser, the main entry point method which calls `ReactDOM.render`, is added as a global window method, `renderCommentBox` in `components/browser.js`, which is used as the entry script for `webpack`. This is all compiled into `public/scripts/bundle.js` which is what the browser includes. The settings for the `webpack` are found in `webpack.config.js`; several libraries like React itself and the markdown parser are set to be externals, which means they are not bundled up so that the browser can load those from a CDN (Content Delivery Network). -On the browser, `example.js` itself is `require`d, after the `node-jsx` module is set up, in order that the JSX syntax can be understood. `example.js` also exports a `renderServer` which returns a static string after calling `ReactDOMServer.renderToString` from the `react-dom/server` module. The route for `/` simply calls this method and passes it as a variable to the `views/index.html` view. +On the browser, `components/server.js` is `require`d, after the `node-jsx` module is set up, in order that the JSX syntax can be understood. This exports a `renderCommentBox` method which returns a static string after calling `ReactDOMServer.renderToString` from the `react-dom/server` module. The route for `/` simply calls this method and passes it as a variable to the `views/index.html` view. ## To use diff --git a/components/browser.js b/components/browser.js new file mode 100644 index 00000000..9ec86d28 --- /dev/null +++ b/components/browser.js @@ -0,0 +1,28 @@ +var components = require('./components'); +var ReactDOM = require('react-dom'); + +/** + * Load the comments via AJAX before rendering the comment box with the DOM. + * This will avoid the server rendered comments being replaced with nothing by JS. + * If the AJAX call fails, then just render no comments after logging the error. + */ +window.renderCommentBox = () => { + var url = "/api/comments"; + + $.get(url).then( + (comments) => { + return comments; + }, + (err) => { + console.error(url, err); + return []; + } + ) + .always((comments) => { + ReactDOM.render(React.createElement(components.CommentBox, { + url: url, + pollInterval: 2000, + comments: comments + }), document.getElementById('content')); + }); +}; diff --git a/src/example.js b/components/components.js similarity index 71% rename from src/example.js rename to components/components.js index 15e6cb2d..81ad6140 100644 --- a/src/example.js +++ b/components/components.js @@ -13,8 +13,7 @@ var templates = require('./templates.jsx'); var React = require('react'); var marked = require('marked'); - -var ReactDOMServer = typeof window === "undefined" ? require('react-dom/server') : undefined; +var $ = require('jquery'); var Comment = React.createClass({ displayName: 'Comment', @@ -34,7 +33,7 @@ var CommentBox = React.createClass({ this.setState({data: data}); }, (err) => { - console.error(this.props.url, err.status, err.statusText); + console.error(this.props.url, err); } ); }, @@ -104,44 +103,9 @@ var CommentForm = React.createClass({ render: templates.commentForm }); - module.exports = { - /** - * Load the comments via AJAX before rendering the comment box with the DOM. - * This will avoid the server rendered comments being replaced with nothing by JS. - * If the AJAX call fails, then just render no comments after logging the error. - */ - render: () => { - var url = "/api/comments"; - - $.get(url).then( - (comments) => { - return comments; - }, - (err) => { - console.error(this.props.url, err.status, err.statusText); - return []; - } - ) - .always((comments) => { - ReactDOM.render(React.createElement(CommentBox, { - url: url, - pollInterval: 2000, - comments: comments - }), document.getElementById('content')); - }); - }, - /** - * Just return a static string to render on the server - * - * @param {Array} comments - * @returns {String} - */ - renderServer: (comments) => { - return ReactDOMServer.renderToString(React.createElement(CommentBox, { - url: "/api/comments", - pollInterval: 2000, - comments: comments - })); - } + Comment, + CommentBox, + CommentList, + CommentForm }; diff --git a/components/server.js b/components/server.js new file mode 100644 index 00000000..f0a162ca --- /dev/null +++ b/components/server.js @@ -0,0 +1,15 @@ +var components = require('./components'); +var React = require('react'); +var ReactDOMServer = require('react-dom/server'); + +module.exports = { + /** + * Just return a static string to render on the server + * + * @param {Object} params + * @returns {String} + */ + renderCommentBox: (params) => { + return ReactDOMServer.renderToString(React.createElement(components.CommentBox, params)); + } +}; \ No newline at end of file diff --git a/src/templates.jsx b/components/templates.jsx similarity index 100% rename from src/templates.jsx rename to components/templates.jsx diff --git a/package.json b/package.json index c9f367b5..b5f616ce 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "body-parser": "^1.4.3", "express": "^4.4.5", + "jquery": "^2.2.0", "marked": "^0.3.5", "node-jsx": "^0.13.3", "react": "^0.14.7", diff --git a/server.js b/server.js index d0097c10..9e64d0b7 100644 --- a/server.js +++ b/server.js @@ -17,7 +17,7 @@ var bodyParser = require('body-parser'); var swig = require('swig'); require('node-jsx').install(); -var example = require('./src/example'); +var components = require('./components/server'); var app = express(); @@ -43,7 +43,9 @@ app.get('/', function(req, res) { } res.render('index', { - commentBox: example.renderServer(JSON.parse(data)) + commentBox: components.renderCommentBox({ + comments: JSON.parse(data) + }) }); }); }); diff --git a/src/index.js b/src/index.js deleted file mode 100644 index c4a1da58..00000000 --- a/src/index.js +++ /dev/null @@ -1 +0,0 @@ -window.commentBoxRender = require('./example.js').render; \ No newline at end of file diff --git a/views/index.html b/views/index.html index dda3f7ed..a93558cd 100644 --- a/views/index.html +++ b/views/index.html @@ -17,6 +17,6 @@ {{ commentBox }} {% endautoescape %} - + diff --git a/webpack.config.js b/webpack.config.js index d5c9900c..ad50f1e2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,11 +1,8 @@ var webpack = require("webpack"); var isDev = process.argv.indexOf('--dev') > -1; -// Don't include react-dom/server in the browser -var ignore = new webpack.IgnorePlugin(/react-dom/); - module.exports = { - entry: "./src/index.js", + entry: "./components/browser.js", output: { path: "./public/scripts", filename: "bundle.js" @@ -13,11 +10,13 @@ module.exports = { // These libraries are included in separate script tags and are available as global variables externals: { "react": "React", - "marked": "marked" + "marked": "marked", + "jquery": "jQuery", + "react-dom": "ReactDOM" }, plugins: isDev ? - [ignore] : - [ignore, new webpack.optimize.UglifyJsPlugin({minimize: true})], + [] : + [new webpack.optimize.UglifyJsPlugin({minimize: true})], devtool: isDev ? 'source-map' : null, module: { loaders: [ From 24a1050e5510dcab4df50824b3e3e1cfd31ffb73 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Tue, 9 Feb 2016 16:37:37 +0000 Subject: [PATCH 05/11] Made the dependencies of browser.js all injected through require --- README.md | 4 ++-- comments.json | 12 +++++++++++- components/browser.js | 4 +++- components/server.js | 4 ++-- webpack.config.js | 5 +++-- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 900c240f..bdac9334 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repository shows how React can be used with the Express framework isomorphically; that is to use the same code on both the server and browser. This uses the comment box example from the [React tutorial](http://facebook.github.io/react/docs/tutorial.html) but in addition to rendering the comment box in the browser, it pre-renders it on the server, using the same code. -There are also a few other additions. I've set up `webpack` so that it bundles up the code to be used in the browser. I also didn't particularly like the `JSX` template render methods being in the same file as the controller code (despite what React says, they are controllers; they behave exactly the same way as Angular's controllers). And lastly since I had to server-side rendering, I've had to use a view engine. I've chose `Swig` as unlike `Jade` it uses proper HTML and so means I have one fewer language to learn (which fits into the philosophy of isomorphism). +There are also a few other additions. I've set up `webpack` so that it bundles up the code to be used in the browser. I also didn't particularly like the `JSX` template render methods being in the same file as the controller / component code (despite what React says, they are not just "views"; they behave very similar to Angular's controllers). And lastly since I had to server-side rendering, I've had to use a view engine. I've chose `Swig` as unlike `Jade` it uses proper HTML and so means I have one fewer language to learn (which fits into the philosophy of isomorphism). Naturally this means all of the other server language implementations have been removed - Python, PHP etc. @@ -30,4 +30,4 @@ And then start the server: npm start -And visit . \ No newline at end of file +And visit . diff --git a/comments.json b/comments.json index 7bef77ad..15cdf664 100644 --- a/comments.json +++ b/comments.json @@ -8,5 +8,15 @@ "id": 1420070400000, "author": "Paul O’Shannessy", "text": "React is *great*!" + }, + { + "id": 1455027234880, + "author": "sdfgsd", + "text": "sdfg" + }, + { + "id": 1455035719653, + "author": "dddd", + "text": "dd" } -] +] \ No newline at end of file diff --git a/components/browser.js b/components/browser.js index 9ec86d28..172284ac 100644 --- a/components/browser.js +++ b/components/browser.js @@ -1,12 +1,14 @@ var components = require('./components'); var ReactDOM = require('react-dom'); +var window = require('window'); +var $ = require('jquery'); /** * Load the comments via AJAX before rendering the comment box with the DOM. * This will avoid the server rendered comments being replaced with nothing by JS. * If the AJAX call fails, then just render no comments after logging the error. */ -window.renderCommentBox = () => { +window.renderCommentBox = function renderCommentBox() { var url = "/api/comments"; $.get(url).then( diff --git a/components/server.js b/components/server.js index f0a162ca..f8f9e0c0 100644 --- a/components/server.js +++ b/components/server.js @@ -9,7 +9,7 @@ module.exports = { * @param {Object} params * @returns {String} */ - renderCommentBox: (params) => { + renderCommentBox(params) { return ReactDOMServer.renderToString(React.createElement(components.CommentBox, params)); } -}; \ No newline at end of file +}; diff --git a/webpack.config.js b/webpack.config.js index ad50f1e2..d9fcd6cf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,7 +12,8 @@ module.exports = { "react": "React", "marked": "marked", "jquery": "jQuery", - "react-dom": "ReactDOM" + "react-dom": "ReactDOM", + "window": "window" }, plugins: isDev ? [] : @@ -30,4 +31,4 @@ module.exports = { } ] } -}; \ No newline at end of file +}; From 8d5227c7fd3378745aaab7a75371ffca346977ae Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Thu, 11 Feb 2016 02:56:56 +0000 Subject: [PATCH 06/11] Hacky validation stuff --- comments.json | 32 +++++++++++++- components/components.js | 89 +++++++++++++++++++++++++++++++++++---- components/templates.jsx | 43 ++++++++++++++----- package.json | 6 ++- public/css/base.css | 4 ++ public/scripts/.gitignore | 4 +- 6 files changed, 155 insertions(+), 23 deletions(-) diff --git a/comments.json b/comments.json index 7bef77ad..647ba759 100644 --- a/comments.json +++ b/comments.json @@ -8,5 +8,35 @@ "id": 1420070400000, "author": "Paul O’Shannessy", "text": "React is *great*!" + }, + { + "id": 1455145027806, + "author": "dfgh", + "text": "dgh" + }, + { + "id": 1455145275989, + "author": "dfghdf", + "text": "ghfgjh" + }, + { + "id": 1455145280188, + "author": "fghjfgj", + "text": "fgjh" + }, + { + "id": 1455152836500, + "author": "sdfgsd", + "text": "sdfg@sadf.com" + }, + { + "id": 1455154018698, + "author": "sdfg", + "text": "sdfg@asdf.com" + }, + { + "id": 1455154150577, + "author": "sdfgsdfg", + "text": "sdsdfgsdfgsdf" } -] +] \ No newline at end of file diff --git a/components/components.js b/components/components.js index 81ad6140..10b20440 100644 --- a/components/components.js +++ b/components/components.js @@ -14,6 +14,10 @@ var templates = require('./templates.jsx'); var React = require('react'); var marked = require('marked'); var $ = require('jquery'); +var classNames = require('classnames'); + +var validation = require('react-validation-mixin'); +var Validator = require('validatorjs'); var Comment = React.createClass({ displayName: 'Comment', @@ -82,27 +86,94 @@ var CommentList = React.createClass({ var CommentForm = React.createClass({ displayName: 'CommentForm', getInitialState: function () { + // Define the rules and custom messages for each field. + // Do this by creating a validator with no data; it's added later. + this.validatorTypes = new Validator( + {}, + { + + }, + { + 'min.text': 'Enter a message between 10 and 50 characters', + 'max.text': 'Enter a message between 10 and 50 characters' + } + ); + return {author: '', text: ''}; }, + addValidation: function(e) { + this.validatorTypes.rules = this.validatorTypes._parseRules({ + author: 'required', + text: 'required|min:10|max:50' + }); + + this.props.handleValidation('author')(e); + this.props.handleValidation('text')(e); + }, handleAuthorChange: function (e) { - this.setState({author: e.target.value}); + this.setState({author: e.target.value}, () => { + this.props.handleValidation('author')(e); + }); }, handleTextChange: function (e) { - this.setState({text: e.target.value}); + this.setState({text: e.target.value}, () => { + this.props.handleValidation('text')(e); + }); }, handleSubmit: function (e) { e.preventDefault(); - var author = this.state.author.trim(); - var text = this.state.text.trim(); - if (!text || !author) { - return; - } - this.props.onCommentSubmit({author: author, text: text}); - this.setState({author: '', text: ''}); + + // If the form is valid, then submit the comment + this.props.validate((error) => { + if (!error) { + this.props.onCommentSubmit(this.state); + this.setState({author: '', text: ''}); + } + }); + }, + getValidatorData: function () { + return this.state; + }, + getClasses: function (field) { + return classNames({ + 'has-error': !this.props.isValid(field) + }); }, render: templates.commentForm }); +var strategy = { + /** + * Validate using the validatorjs library + * + * @see https://www.npmjs.com/package/validatorjs + * + * @param {Object} data the data submitted + * @param {Validator} validator the validatorjs validator + * @param {Object} options contains name of element being validated and previous errors + * @param {Function} callback called and passed the errors + */ + validate: function(data, validator, options, callback) { + // Set the data again on the validator and clear existing errors + validator.input = data; + validator.errors.errors = {}; + + var getErrors = () => { + if (options.key) { + options.prevErrors[options.key] = validator.errors.get(options.key); + callback(options.prevErrors); + } else { + callback(validator.errors.all()); + } + }; + + // Run the validator asynchronously in case any async rules have been added + validator.checkAsync(getErrors, getErrors); + } +}; + +CommentForm = validation(strategy)(CommentForm); + module.exports = { Comment, CommentBox, diff --git a/components/templates.jsx b/components/templates.jsx index f71096b8..e2a7ef6d 100644 --- a/components/templates.jsx +++ b/components/templates.jsx @@ -36,20 +36,43 @@ module.exports = { ); }, commentForm: function () { + function renderErrors(messages) { + if (messages.length) { + messages = messages.map((message) =>
  • {message}
  • ); + + return
      {messages}
    ; + } + } + return (
    - + - + + {renderErrors(this.props.getValidationMessages('author'))} + +

    + +

    + + {renderErrors(this.props.getValidationMessages('text'))} +
    ); diff --git a/package.json b/package.json index b5f616ce..fff4f2be 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,17 @@ "main": "server.js", "dependencies": { "body-parser": "^1.4.3", + "classnames": "^2.2.3", "express": "^4.4.5", + "install": "^0.4.2", "jquery": "^2.2.0", "marked": "^0.3.5", "node-jsx": "^0.13.3", "react": "^0.14.7", "react-dom": "^0.14.7", - "swig": "^1.4.2" + "react-validation-mixin": "^5.3.4", + "swig": "^1.4.2", + "validatorjs": "^2.0.2" }, "devDependencies": { "babel-core": "^6.4.5", diff --git a/public/css/base.css b/public/css/base.css index 08de8f1b..5366cb6f 100644 --- a/public/css/base.css +++ b/public/css/base.css @@ -60,3 +60,7 @@ p, ul { ul { padding-left: 30px; } + +.has-error { + border: 1px solid red; +} \ No newline at end of file diff --git a/public/scripts/.gitignore b/public/scripts/.gitignore index c96a04f0..5eab0140 100644 --- a/public/scripts/.gitignore +++ b/public/scripts/.gitignore @@ -1,2 +1,2 @@ -* -!.gitignore \ No newline at end of file +bundle.js +bundle.js.map \ No newline at end of file From c3bfdc1fd251f10352940d173a0698de85487cbb Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Thu, 11 Feb 2016 17:31:47 +0000 Subject: [PATCH 07/11] In the middle of validation work --- components/components.js | 80 +++++++++++++++++++++++++++------------- components/templates.jsx | 6 ++- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/components/components.js b/components/components.js index 10b20440..1b0de1e1 100644 --- a/components/components.js +++ b/components/components.js @@ -10,6 +10,8 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +'use strict'; + var templates = require('./templates.jsx'); var React = require('react'); var marked = require('marked'); @@ -87,37 +89,29 @@ var CommentForm = React.createClass({ displayName: 'CommentForm', getInitialState: function () { // Define the rules and custom messages for each field. - // Do this by creating a validator with no data; it's added later. - this.validatorTypes = new Validator( - {}, + this.validatorTypes = strategy.createInactiveSchema( { - + author: 'required', + text: 'required|min:10|max:50' }, { - 'min.text': 'Enter a message between 10 and 50 characters', - 'max.text': 'Enter a message between 10 and 50 characters' + //'min.text': 'Enter a message between 10 and 50 characters', + //'max.text': 'Enter a message between 10 and 50 characters' } ); return {author: '', text: ''}; }, addValidation: function(e) { - this.validatorTypes.rules = this.validatorTypes._parseRules({ - author: 'required', - text: 'required|min:10|max:50' - }); - - this.props.handleValidation('author')(e); - this.props.handleValidation('text')(e); + strategy.activateRule(this.validatorTypes, e.target.name); + this.props.handleValidation(e.target.name)(e); }, - handleAuthorChange: function (e) { - this.setState({author: e.target.value}, () => { - this.props.handleValidation('author')(e); - }); - }, - handleTextChange: function (e) { - this.setState({text: e.target.value}, () => { - this.props.handleValidation('text')(e); + handleChange: function (e) { + var state = {}; + state[e.target.name] = e.target.value; + + this.setState(state, () => { + this.props.handleValidation(e.target.name)(e); }); }, handleSubmit: function (e) { @@ -143,20 +137,54 @@ var CommentForm = React.createClass({ }); var strategy = { + createSchema: function (rules, messages, createValidatorCallback) { + return { + rules, + messages, + createValidatorCallback + }; + }, + createInactiveSchema: function (rules, messages, createValidatorCallback) { + var schema = this.createSchema(rules, messages, createValidatorCallback); + schema.activeRules = []; + + return schema; + }, + activateRule: function(schema, rule) { + if (schema.activeRules.indexOf(rule) === -1) { + schema.activeRules.push(rule); + } + }, /** * Validate using the validatorjs library * * @see https://www.npmjs.com/package/validatorjs * * @param {Object} data the data submitted - * @param {Validator} validator the validatorjs validator + * @param {Object} schema contains rules and custom error messages * @param {Object} options contains name of element being validated and previous errors * @param {Function} callback called and passed the errors */ - validate: function(data, validator, options, callback) { - // Set the data again on the validator and clear existing errors - validator.input = data; - validator.errors.errors = {}; + validate: function (data, schema, options, callback) { + var rules = {}; + + // Only add active rules to the validator if an initially inactive schema has been created. + // Check all rules regardless if the form has been submitted (options.key is empty). + if (typeof schema.activeRules !== 'undefined' && options.key) { + for (let i in schema.activeRules) { + let ruleName = schema.activeRules[i]; + + rules[ruleName] = schema.rules[ruleName]; + } + } else { + rules = schema.rules; + } + + var validator = new Validator(data, rules, schema.messages); + + if (typeof schema.createValidatorCallback === 'function') { + schema.createValidatorCallback(validator); + } var getErrors = () => { if (options.key) { diff --git a/components/templates.jsx b/components/templates.jsx index e2a7ef6d..9c47cc11 100644 --- a/components/templates.jsx +++ b/components/templates.jsx @@ -50,9 +50,10 @@ module.exports = {

    @@ -63,9 +64,10 @@ module.exports = { From a567747e736f6de153ca9913977b0b0a71ca1486 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Fri, 12 Feb 2016 01:28:11 +0000 Subject: [PATCH 08/11] Added server side validation --- README.md | 14 +++- comments.json | 30 --------- components/components.js | 104 +++++++---------------------- components/schemas.js | 14 ++++ components/strategy.js | 138 +++++++++++++++++++++++++++++++++++++++ components/templates.jsx | 4 +- package.json | 4 +- server.js | 43 +++++++----- 8 files changed, 217 insertions(+), 134 deletions(-) create mode 100644 components/schemas.js create mode 100644 components/strategy.js diff --git a/README.md b/README.md index bdac9334..6b74bed3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # Isomorphic React and Express -This repository shows how React can be used with the Express framework isomorphically; that is to use the same code on both the server and browser. This uses the comment box example from the [React tutorial](http://facebook.github.io/react/docs/tutorial.html) but in addition to rendering the comment box in the browser, it pre-renders it on the server, using the same code. +This repository shows how React can be used with the Express framework isomorphically; that is to use the same code on both the server and browser to render the same sections of pages. This uses the comment box example from the [React tutorial](http://facebook.github.io/react/docs/tutorial.html) but in addition to rendering the comment box in the browser, it pre-renders it on the server, using the same code. There are also a few other additions. I've set up `webpack` so that it bundles up the code to be used in the browser. I also didn't particularly like the `JSX` template render methods being in the same file as the controller / component code (despite what React says, they are not just "views"; they behave very similar to Angular's controllers). And lastly since I had to server-side rendering, I've had to use a view engine. I've chose `Swig` as unlike `Jade` it uses proper HTML and so means I have one fewer language to learn (which fits into the philosophy of isomorphism). +Isomorphic validation is also available: see more information below under `Validation`. + Naturally this means all of the other server language implementations have been removed - Python, PHP etc. ## Implementation @@ -14,13 +16,19 @@ In the browser, the main entry point method which calls `ReactDOM.render`, is ad On the browser, `components/server.js` is `require`d, after the `node-jsx` module is set up, in order that the JSX syntax can be understood. This exports a `renderCommentBox` method which returns a static string after calling `ReactDOMServer.renderToString` from the `react-dom/server` module. The route for `/` simply calls this method and passes it as a variable to the `views/index.html` view. +## Validation + +There is also client-side and server-side validation which again is isomorphic. This uses `react-validation-mixin` for React from which the rules are defined by `validatorjs`. These are connected using a "strategy" defined in `components/strategy.js` where more information can be found. The schemas are defined in `components/schemas.js`. + +Extra functionality has been added to `components/templates.jsx` and `components/components.js` to handle the validation and server-side to the `/api/comments` POST end point in `server.js`. + ## To use npm install Next, to compile the browser code into `public/scripts/bundle.js`: - npm run webpack-dev + npm run webpack:dev Or if you want to compile it to a minified version without the source map: @@ -30,4 +38,4 @@ And then start the server: npm start -And visit . +And visit . diff --git a/comments.json b/comments.json index 647ba759..393dce6e 100644 --- a/comments.json +++ b/comments.json @@ -8,35 +8,5 @@ "id": 1420070400000, "author": "Paul O’Shannessy", "text": "React is *great*!" - }, - { - "id": 1455145027806, - "author": "dfgh", - "text": "dgh" - }, - { - "id": 1455145275989, - "author": "dfghdf", - "text": "ghfgjh" - }, - { - "id": 1455145280188, - "author": "fghjfgj", - "text": "fgjh" - }, - { - "id": 1455152836500, - "author": "sdfgsd", - "text": "sdfg@sadf.com" - }, - { - "id": 1455154018698, - "author": "sdfg", - "text": "sdfg@asdf.com" - }, - { - "id": 1455154150577, - "author": "sdfgsdfg", - "text": "sdsdfgsdfgsdf" } ] \ No newline at end of file diff --git a/components/components.js b/components/components.js index 1b0de1e1..8e2db074 100644 --- a/components/components.js +++ b/components/components.js @@ -10,16 +10,16 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -'use strict'; -var templates = require('./templates.jsx'); var React = require('react'); var marked = require('marked'); -var $ = require('jquery'); var classNames = require('classnames'); - var validation = require('react-validation-mixin'); -var Validator = require('validatorjs'); +var $ = require('jquery'); + +var templates = require('./templates.jsx'); +var strategy = require('./strategy'); +var schemas = require('./schemas'); var Comment = React.createClass({ displayName: 'Comment', @@ -89,23 +89,24 @@ var CommentForm = React.createClass({ displayName: 'CommentForm', getInitialState: function () { // Define the rules and custom messages for each field. - this.validatorTypes = strategy.createInactiveSchema( - { - author: 'required', - text: 'required|min:10|max:50' - }, - { - //'min.text': 'Enter a message between 10 and 50 characters', - //'max.text': 'Enter a message between 10 and 50 characters' - } - ); + this.validatorTypes = schemas.commentForm; return {author: '', text: ''}; }, - addValidation: function(e) { + /** + * Activate the validation rule for the element on blur + * + * @param {Event} e + */ + activateValidation: function(e) { strategy.activateRule(this.validatorTypes, e.target.name); this.props.handleValidation(e.target.name)(e); }, + /** + * Set the state of the changed variable and then when set, call validator + * + * @param {Event} e + */ handleChange: function (e) { var state = {}; state[e.target.name] = e.target.value; @@ -114,12 +115,17 @@ var CommentForm = React.createClass({ this.props.handleValidation(e.target.name)(e); }); }, + /** + * Validate the form and if valid, submit the comment + * + * @param {Event} e + */ handleSubmit: function (e) { e.preventDefault(); - // If the form is valid, then submit the comment this.props.validate((error) => { if (!error) { + // this.props.onCommentSubmit is actually CommentBox.handleCommentSubmit this.props.onCommentSubmit(this.state); this.setState({author: '', text: ''}); } @@ -136,70 +142,6 @@ var CommentForm = React.createClass({ render: templates.commentForm }); -var strategy = { - createSchema: function (rules, messages, createValidatorCallback) { - return { - rules, - messages, - createValidatorCallback - }; - }, - createInactiveSchema: function (rules, messages, createValidatorCallback) { - var schema = this.createSchema(rules, messages, createValidatorCallback); - schema.activeRules = []; - - return schema; - }, - activateRule: function(schema, rule) { - if (schema.activeRules.indexOf(rule) === -1) { - schema.activeRules.push(rule); - } - }, - /** - * Validate using the validatorjs library - * - * @see https://www.npmjs.com/package/validatorjs - * - * @param {Object} data the data submitted - * @param {Object} schema contains rules and custom error messages - * @param {Object} options contains name of element being validated and previous errors - * @param {Function} callback called and passed the errors - */ - validate: function (data, schema, options, callback) { - var rules = {}; - - // Only add active rules to the validator if an initially inactive schema has been created. - // Check all rules regardless if the form has been submitted (options.key is empty). - if (typeof schema.activeRules !== 'undefined' && options.key) { - for (let i in schema.activeRules) { - let ruleName = schema.activeRules[i]; - - rules[ruleName] = schema.rules[ruleName]; - } - } else { - rules = schema.rules; - } - - var validator = new Validator(data, rules, schema.messages); - - if (typeof schema.createValidatorCallback === 'function') { - schema.createValidatorCallback(validator); - } - - var getErrors = () => { - if (options.key) { - options.prevErrors[options.key] = validator.errors.get(options.key); - callback(options.prevErrors); - } else { - callback(validator.errors.all()); - } - }; - - // Run the validator asynchronously in case any async rules have been added - validator.checkAsync(getErrors, getErrors); - } -}; - CommentForm = validation(strategy)(CommentForm); module.exports = { diff --git a/components/schemas.js b/components/schemas.js new file mode 100644 index 00000000..2f805352 --- /dev/null +++ b/components/schemas.js @@ -0,0 +1,14 @@ +var strategy = require('./strategy'); + +module.exports = { + commentForm: strategy.createInactiveSchema( + { + author: 'required', + text: 'required|min:10|max:50' + }, + { + 'min.text': 'Enter a message between 10 and 50 characters', + 'max.text': 'Enter a message between 10 and 50 characters' + } + ) +}; diff --git a/components/strategy.js b/components/strategy.js new file mode 100644 index 00000000..513a2773 --- /dev/null +++ b/components/strategy.js @@ -0,0 +1,138 @@ +/** + * Validate using the validatorjs library as a strategy for react-validation-mixin + * + * @see https://www.npmjs.com/package/validatorjs + * @see https://www.npmjs.com/package/react-validator-mixin + */ + +'use strict'; + +var Validator = require('validatorjs'); + +module.exports = { + /** + * Used to create this.validatorTypes in a React component and to be passed to validate or validateServer + * + * @param {Object} rules List of rules as specified by validatorjs + * @param {Object} messages Optional list of custom messages as specified by validatorjs + * @param {Function} callback if specified, called to allow customisation of validator + * @returns {Object} + */ + createSchema: function (rules, messages, callback) { + return { + rules, + messages, + callback + }; + }, + /** + * Same as createSchema, but the rules are disabled until activateRule is called + * + * @param {Object} rules List of rules as specified by validatorjs + * @param {Object} messages Optional list of custom messages as specified by validatorjs + * @param {Function} callback if specified, called to allow customisation of validator + * @returns {Object} + */ + createInactiveSchema: function (rules, messages, callback) { + var schema = this.createSchema(rules, messages, callback); + schema.activeRules = []; + + return schema; + }, + /** + * Active a specific rule + * + * @param {Object} schema As created by createInactiveSchema + * @param {Object} rule Name of the rule as a key in schema.rules + */ + activateRule: function(schema, rule) { + if (typeof schema.activeRules !== 'undefined' && schema.activeRules.indexOf(rule) === -1) { + schema.activeRules.push(rule); + } + }, + /** + * Create a validator from submitted data and a schema + * + * @param {Object} data The data submitted + * @param {Object} schema Contains rules and custom error messages + * @param {Boolean} forceActive Whether to force all rules to be active even if not activated + * @returns {Validator} + */ + createValidator: function (data, schema, forceActive) { + var rules = {}; + + // Only add active rules to the validator if an initially inactive schema has been created. + if (typeof schema.activeRules !== 'undefined') { + // Force all rules to be active if specified + if (forceActive) { + schema.activeRules = Object.keys(schema.rules); + } + + for (let i in schema.activeRules) { + let ruleName = schema.activeRules[i]; + + rules[ruleName] = schema.rules[ruleName]; + } + } else { + rules = schema.rules; + } + + var validator = new Validator(data, rules, schema.messages); + + // If a callback has been specified on the schema, call it to allow customisation of the validator + if (typeof schema.callback === 'function') { + schema.callback(validator); + } + + return validator; + }, + /** + * Called by react-validation-mixin + * + * @param {Object} data The data submitted + * @param {Object} schema Contains rules and custom error messages + * @param {Object} options Contains name of element being validated and previous errors + * @param {Function} callback Called and passed the errors after validation + */ + validate: function (data, schema, options, callback) { + // If the whole form has been submitted, then activate all rules + var forceActive = !options.key; + var validator = this.createValidator(data, schema, forceActive); + + var getErrors = () => { + // If a single element is being validated, just get those errors. + // Otherwise get all of them. + if (options.key) { + options.prevErrors[options.key] = validator.errors.get(options.key); + callback(options.prevErrors); + } else { + callback(validator.errors.all()); + } + }; + + // Run the validator asynchronously in case any async rules have been added + validator.checkAsync(getErrors, getErrors); + }, + /** + * Validate server-side returning a Promise to easier handle results. + * All inactive rules will be forced to activate. + * + * @param {Object} data The data submitted + * @param {Object} schema Contains rules and custom error messages + * @returns {Promise} + */ + validateServer: function (data, schema) { + var validator = this.createValidator(data, schema, true); + + return new Promise((resolve, reject) => { + validator.checkAsync( + () => { + resolve(); + }, + () => { + reject(validator.errors.all()); + } + ); + }); + } +}; diff --git a/components/templates.jsx b/components/templates.jsx index 9c47cc11..3cd2175a 100644 --- a/components/templates.jsx +++ b/components/templates.jsx @@ -54,7 +54,7 @@ module.exports = { className={this.getClasses('author')} value={this.state.author} onChange={this.handleChange} - onBlur={this.addValidation} + onBlur={this.activateValidation} />

    @@ -68,7 +68,7 @@ module.exports = { className={this.getClasses('text')} value={this.state.text} onChange={this.handleChange} - onBlur={this.addValidation} + onBlur={this.activateValidation} />

    diff --git a/package.json b/package.json index fff4f2be..a20d4bec 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "jquery": "^2.2.0", "marked": "^0.3.5", "node-jsx": "^0.13.3", + "nodemon": "^1.8.1", "react": "^0.14.7", "react-dom": "^0.14.7", "react-validation-mixin": "^5.3.4", @@ -28,7 +29,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js", - "webpack-dev": "webpack --watch --dev", + "watch": "nodemon -e js,html server.js", + "webpack:dev": "webpack --watch --dev", "webpack": "webpack" }, "repository": { diff --git a/server.js b/server.js index 9e64d0b7..43d38dd5 100644 --- a/server.js +++ b/server.js @@ -19,6 +19,9 @@ var swig = require('swig'); require('node-jsx').install(); var components = require('./components/server'); +var strategy = require('./components/strategy'); +var schemas = require('./components/schemas'); + var app = express(); app.engine('html', swig.renderFile); @@ -72,28 +75,34 @@ app.get('/api/comments', function(req, res) { }); app.post('/api/comments', function(req, res) { - fs.readFile(COMMENTS_FILE, function(err, data) { - if (err) { - console.error(err); - process.exit(1); - } - var comments = JSON.parse(data); - // NOTE: In a real implementation, we would likely rely on a database or - // some other approach (e.g. UUIDs) to ensure a globally unique id. We'll - // treat Date.now() as unique-enough for our purposes. - var newComment = { - id: Date.now(), - author: req.body.author, - text: req.body.text, - }; - comments.push(newComment); - fs.writeFile(COMMENTS_FILE, JSON.stringify(comments, null, 4), function(err) { + strategy.validateServer(req.body, schemas.commentForm).then(() => { + fs.readFile(COMMENTS_FILE, function(err, data) { if (err) { console.error(err); process.exit(1); } - res.json(comments); + var comments = JSON.parse(data); + // NOTE: In a real implementation, we would likely rely on a database or + // some other approach (e.g. UUIDs) to ensure a globally unique id. We'll + // treat Date.now() as unique-enough for our purposes. + var newComment = { + id: Date.now(), + author: req.body.author, + text: req.body.text, + }; + comments.push(newComment); + fs.writeFile(COMMENTS_FILE, JSON.stringify(comments, null, 4), function(err) { + if (err) { + console.error(err); + process.exit(1); + } + res.json(comments); + }); }); + }) + .catch((errors) => { + // Handle validation errors + res.status(400).json(errors); }); }); From fe62ce63ce0ef5c3dff1c5bcab830f30bacacb2c Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Fri, 12 Feb 2016 23:35:29 +0000 Subject: [PATCH 09/11] The validation error server-side now returns a custom Error through the promise, which the error handler middleware handles --- components/browser.js | 2 +- components/components.js | 31 ++++++++++++++----------------- components/strategy.js | 16 +++++++++++++--- components/templates.jsx | 12 ++++++------ package.json | 1 - server.js | 20 ++++++++++++++------ 6 files changed, 48 insertions(+), 34 deletions(-) diff --git a/components/browser.js b/components/browser.js index 172284ac..cd6e31c5 100644 --- a/components/browser.js +++ b/components/browser.js @@ -8,7 +8,7 @@ var $ = require('jquery'); * This will avoid the server rendered comments being replaced with nothing by JS. * If the AJAX call fails, then just render no comments after logging the error. */ -window.renderCommentBox = function renderCommentBox() { +window.renderCommentBox = function () { var url = "/api/comments"; $.get(url).then( diff --git a/components/components.js b/components/components.js index 8e2db074..f3938815 100644 --- a/components/components.js +++ b/components/components.js @@ -13,7 +13,6 @@ var React = require('react'); var marked = require('marked'); -var classNames = require('classnames'); var validation = require('react-validation-mixin'); var $ = require('jquery'); @@ -23,7 +22,7 @@ var schemas = require('./schemas'); var Comment = React.createClass({ displayName: 'Comment', - rawMarkup: function () { + rawMarkup() { var rawMarkup = marked(this.props.children.toString(), {sanitize: true}); return {__html: rawMarkup}; }, @@ -33,7 +32,7 @@ var Comment = React.createClass({ var CommentBox = React.createClass({ displayName: 'CommentBox', - loadCommentsFromServer: function () { + loadCommentsFromServer() { $.get(this.props.url).then( (data) => { this.setState({data: data}); @@ -43,7 +42,7 @@ var CommentBox = React.createClass({ } ); }, - handleCommentSubmit: function (comment) { + handleCommentSubmit(comment) { var comments = this.state.data; // Optimistically set an id on the new comment. It will be replaced by an // id generated by the server. In a production application you would likely @@ -66,28 +65,28 @@ var CommentBox = React.createClass({ }.bind(this) }); }, - getInitialState: function () { + getInitialState() { return {data: this.props.comments}; }, - componentDidMount: function () { + componentDidMount() { this.loadCommentsFromServer(); setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, - render: function () { + render() { return templates.commentBox.apply(this, [CommentList, CommentForm]); } }); var CommentList = React.createClass({ displayName: 'CommentList', - render: function () { + render() { return templates.commentList.apply(this, [Comment]); } }); var CommentForm = React.createClass({ displayName: 'CommentForm', - getInitialState: function () { + getInitialState() { // Define the rules and custom messages for each field. this.validatorTypes = schemas.commentForm; @@ -98,7 +97,7 @@ var CommentForm = React.createClass({ * * @param {Event} e */ - activateValidation: function(e) { + activateValidation(e) { strategy.activateRule(this.validatorTypes, e.target.name); this.props.handleValidation(e.target.name)(e); }, @@ -107,7 +106,7 @@ var CommentForm = React.createClass({ * * @param {Event} e */ - handleChange: function (e) { + handleChange(e) { var state = {}; state[e.target.name] = e.target.value; @@ -120,7 +119,7 @@ var CommentForm = React.createClass({ * * @param {Event} e */ - handleSubmit: function (e) { + handleSubmit(e) { e.preventDefault(); this.props.validate((error) => { @@ -131,13 +130,11 @@ var CommentForm = React.createClass({ } }); }, - getValidatorData: function () { + getValidatorData() { return this.state; }, - getClasses: function (field) { - return classNames({ - 'has-error': !this.props.isValid(field) - }); + getClassName(field) { + return this.props.isValid(field) ? '' : 'has-error'; }, render: templates.commentForm }); diff --git a/components/strategy.js b/components/strategy.js index 513a2773..b42f6e97 100644 --- a/components/strategy.js +++ b/components/strategy.js @@ -2,7 +2,7 @@ * Validate using the validatorjs library as a strategy for react-validation-mixin * * @see https://www.npmjs.com/package/validatorjs - * @see https://www.npmjs.com/package/react-validator-mixin + * @see https://jurassix.gitbooks.io/docs-react-validation-mixin/content/overview/strategies.html */ 'use strict'; @@ -130,9 +130,19 @@ module.exports = { resolve(); }, () => { - reject(validator.errors.all()); + var e = new this.Error('A validation error occurred'); + e.errors = validator.errors.all(); + + reject(e); } ); }); - } + }, + /** + * Extension of the built-in Error. Created by validateServer when validation fails. + * Exists so that middleware can check it with instanceof: if (err instanceof strategy.Error) + * + * @property {Object} errors Contains the error messages by field name. + */ + Error: class extends Error {} }; diff --git a/components/templates.jsx b/components/templates.jsx index 3cd2175a..864b7a1e 100644 --- a/components/templates.jsx +++ b/components/templates.jsx @@ -1,7 +1,7 @@ var React = require('react'); module.exports = { - comment: function () { + comment() { return (

    @@ -11,7 +11,7 @@ module.exports = {

    ); }, - commentBox: function (CommentList, CommentForm) { + commentBox(CommentList, CommentForm) { return (

    Comments

    @@ -20,7 +20,7 @@ module.exports = {
    ); }, - commentList: function (Comment) { + commentList(Comment) { var commentNodes = this.props.data.map(function(comment) { return ( @@ -35,7 +35,7 @@ module.exports = { ); }, - commentForm: function () { + commentForm() { function renderErrors(messages) { if (messages.length) { messages = messages.map((message) =>
  • {message}
  • ); @@ -51,7 +51,7 @@ module.exports = { type="text" placeholder="Your name" name="author" - className={this.getClasses('author')} + className={this.getClassName('author')} value={this.state.author} onChange={this.handleChange} onBlur={this.activateValidation} @@ -65,7 +65,7 @@ module.exports = { type="text" placeholder="Say something..." name="text" - className={this.getClasses('text')} + className={this.getClassName('text')} value={this.state.text} onChange={this.handleChange} onBlur={this.activateValidation} diff --git a/package.json b/package.json index a20d4bec..6d354a0f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "main": "server.js", "dependencies": { "body-parser": "^1.4.3", - "classnames": "^2.2.3", "express": "^4.4.5", "install": "^0.4.2", "jquery": "^2.2.0", diff --git a/server.js b/server.js index 43d38dd5..fe23e19f 100644 --- a/server.js +++ b/server.js @@ -37,7 +37,6 @@ app.use(express.static(path.join(__dirname, 'public'))); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: true})); - app.get('/', function(req, res) { fs.readFile(COMMENTS_FILE, function(err, data) { if (err) { @@ -74,7 +73,8 @@ app.get('/api/comments', function(req, res) { }); }); -app.post('/api/comments', function(req, res) { +app.post('/api/comments', function(req, res, next) { + // Validate the request and if it fails, call the error handler strategy.validateServer(req.body, schemas.commentForm).then(() => { fs.readFile(COMMENTS_FILE, function(err, data) { if (err) { @@ -100,12 +100,20 @@ app.post('/api/comments', function(req, res) { }); }); }) - .catch((errors) => { - // Handle validation errors - res.status(400).json(errors); - }); + .catch(next); }); +/** + * If a validation error, output a 400 JSON response containing the error messages. + * Otherwise, use the default error handler. + */ +app.use(function(err, req, res, next) { + if (err instanceof strategy.Error) { + res.status(400).json(err.errors); + } else { + next(err, req, res); + } +}); app.listen(app.get('port'), function() { console.log('Server started: http://localhost:' + app.get('port') + '/'); From 9d687c3e274ab7e3edb828087b52026335b36420 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sun, 14 Feb 2016 14:18:20 +0000 Subject: [PATCH 10/11] es6 syntax --- components/strategy.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/components/strategy.js b/components/strategy.js index b42f6e97..29ee45b6 100644 --- a/components/strategy.js +++ b/components/strategy.js @@ -18,7 +18,7 @@ module.exports = { * @param {Function} callback if specified, called to allow customisation of validator * @returns {Object} */ - createSchema: function (rules, messages, callback) { + createSchema(rules, messages, callback) { return { rules, messages, @@ -33,7 +33,7 @@ module.exports = { * @param {Function} callback if specified, called to allow customisation of validator * @returns {Object} */ - createInactiveSchema: function (rules, messages, callback) { + createInactiveSchema(rules, messages, callback) { var schema = this.createSchema(rules, messages, callback); schema.activeRules = []; @@ -45,7 +45,7 @@ module.exports = { * @param {Object} schema As created by createInactiveSchema * @param {Object} rule Name of the rule as a key in schema.rules */ - activateRule: function(schema, rule) { + activateRule(schema, rule) { if (typeof schema.activeRules !== 'undefined' && schema.activeRules.indexOf(rule) === -1) { schema.activeRules.push(rule); } @@ -58,7 +58,7 @@ module.exports = { * @param {Boolean} forceActive Whether to force all rules to be active even if not activated * @returns {Validator} */ - createValidator: function (data, schema, forceActive) { + createValidator(data, schema, forceActive) { var rules = {}; // Only add active rules to the validator if an initially inactive schema has been created. @@ -94,7 +94,7 @@ module.exports = { * @param {Object} options Contains name of element being validated and previous errors * @param {Function} callback Called and passed the errors after validation */ - validate: function (data, schema, options, callback) { + validate(data, schema, options, callback) { // If the whole form has been submitted, then activate all rules var forceActive = !options.key; var validator = this.createValidator(data, schema, forceActive); @@ -121,7 +121,7 @@ module.exports = { * @param {Object} schema Contains rules and custom error messages * @returns {Promise} */ - validateServer: function (data, schema) { + validateServer(data, schema) { var validator = this.createValidator(data, schema, true); return new Promise((resolve, reject) => { From a0825488052b881815ae09d4f02912d3690696e2 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Tue, 16 Feb 2016 19:12:48 +0000 Subject: [PATCH 11/11] Now uses react-validatorjs-strategy --- README.md | 2 +- components/components.js | 2 +- components/schemas.js | 2 +- components/strategy.js | 148 --------------------------------------- package.json | 1 + server.js | 5 +- 6 files changed, 6 insertions(+), 154 deletions(-) delete mode 100644 components/strategy.js diff --git a/README.md b/README.md index 6b74bed3..e1b7f592 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ On the browser, `components/server.js` is `require`d, after the `node-jsx` modul ## Validation -There is also client-side and server-side validation which again is isomorphic. This uses `react-validation-mixin` for React from which the rules are defined by `validatorjs`. These are connected using a "strategy" defined in `components/strategy.js` where more information can be found. The schemas are defined in `components/schemas.js`. +There is also client-side and server-side validation which again is isomorphic. This uses `react-validation-mixin` for React from which the rules are defined by `validatorjs`. These are connected using a [react-validatorjs-strategy](https://github.com/TheChech/react-validatorjs-strategy). The schemas are defined in `components/schemas.js`. Extra functionality has been added to `components/templates.jsx` and `components/components.js` to handle the validation and server-side to the `/api/comments` POST end point in `server.js`. diff --git a/components/components.js b/components/components.js index f3938815..2f0d1f86 100644 --- a/components/components.js +++ b/components/components.js @@ -15,9 +15,9 @@ var React = require('react'); var marked = require('marked'); var validation = require('react-validation-mixin'); var $ = require('jquery'); +var strategy = require('react-validatorjs-strategy'); var templates = require('./templates.jsx'); -var strategy = require('./strategy'); var schemas = require('./schemas'); var Comment = React.createClass({ diff --git a/components/schemas.js b/components/schemas.js index 2f805352..2092a3df 100644 --- a/components/schemas.js +++ b/components/schemas.js @@ -1,4 +1,4 @@ -var strategy = require('./strategy'); +var strategy = require('react-validatorjs-strategy'); module.exports = { commentForm: strategy.createInactiveSchema( diff --git a/components/strategy.js b/components/strategy.js deleted file mode 100644 index 29ee45b6..00000000 --- a/components/strategy.js +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Validate using the validatorjs library as a strategy for react-validation-mixin - * - * @see https://www.npmjs.com/package/validatorjs - * @see https://jurassix.gitbooks.io/docs-react-validation-mixin/content/overview/strategies.html - */ - -'use strict'; - -var Validator = require('validatorjs'); - -module.exports = { - /** - * Used to create this.validatorTypes in a React component and to be passed to validate or validateServer - * - * @param {Object} rules List of rules as specified by validatorjs - * @param {Object} messages Optional list of custom messages as specified by validatorjs - * @param {Function} callback if specified, called to allow customisation of validator - * @returns {Object} - */ - createSchema(rules, messages, callback) { - return { - rules, - messages, - callback - }; - }, - /** - * Same as createSchema, but the rules are disabled until activateRule is called - * - * @param {Object} rules List of rules as specified by validatorjs - * @param {Object} messages Optional list of custom messages as specified by validatorjs - * @param {Function} callback if specified, called to allow customisation of validator - * @returns {Object} - */ - createInactiveSchema(rules, messages, callback) { - var schema = this.createSchema(rules, messages, callback); - schema.activeRules = []; - - return schema; - }, - /** - * Active a specific rule - * - * @param {Object} schema As created by createInactiveSchema - * @param {Object} rule Name of the rule as a key in schema.rules - */ - activateRule(schema, rule) { - if (typeof schema.activeRules !== 'undefined' && schema.activeRules.indexOf(rule) === -1) { - schema.activeRules.push(rule); - } - }, - /** - * Create a validator from submitted data and a schema - * - * @param {Object} data The data submitted - * @param {Object} schema Contains rules and custom error messages - * @param {Boolean} forceActive Whether to force all rules to be active even if not activated - * @returns {Validator} - */ - createValidator(data, schema, forceActive) { - var rules = {}; - - // Only add active rules to the validator if an initially inactive schema has been created. - if (typeof schema.activeRules !== 'undefined') { - // Force all rules to be active if specified - if (forceActive) { - schema.activeRules = Object.keys(schema.rules); - } - - for (let i in schema.activeRules) { - let ruleName = schema.activeRules[i]; - - rules[ruleName] = schema.rules[ruleName]; - } - } else { - rules = schema.rules; - } - - var validator = new Validator(data, rules, schema.messages); - - // If a callback has been specified on the schema, call it to allow customisation of the validator - if (typeof schema.callback === 'function') { - schema.callback(validator); - } - - return validator; - }, - /** - * Called by react-validation-mixin - * - * @param {Object} data The data submitted - * @param {Object} schema Contains rules and custom error messages - * @param {Object} options Contains name of element being validated and previous errors - * @param {Function} callback Called and passed the errors after validation - */ - validate(data, schema, options, callback) { - // If the whole form has been submitted, then activate all rules - var forceActive = !options.key; - var validator = this.createValidator(data, schema, forceActive); - - var getErrors = () => { - // If a single element is being validated, just get those errors. - // Otherwise get all of them. - if (options.key) { - options.prevErrors[options.key] = validator.errors.get(options.key); - callback(options.prevErrors); - } else { - callback(validator.errors.all()); - } - }; - - // Run the validator asynchronously in case any async rules have been added - validator.checkAsync(getErrors, getErrors); - }, - /** - * Validate server-side returning a Promise to easier handle results. - * All inactive rules will be forced to activate. - * - * @param {Object} data The data submitted - * @param {Object} schema Contains rules and custom error messages - * @returns {Promise} - */ - validateServer(data, schema) { - var validator = this.createValidator(data, schema, true); - - return new Promise((resolve, reject) => { - validator.checkAsync( - () => { - resolve(); - }, - () => { - var e = new this.Error('A validation error occurred'); - e.errors = validator.errors.all(); - - reject(e); - } - ); - }); - }, - /** - * Extension of the built-in Error. Created by validateServer when validation fails. - * Exists so that middleware can check it with instanceof: if (err instanceof strategy.Error) - * - * @property {Object} errors Contains the error messages by field name. - */ - Error: class extends Error {} -}; diff --git a/package.json b/package.json index 6d354a0f..0208c794 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "react": "^0.14.7", "react-dom": "^0.14.7", "react-validation-mixin": "^5.3.4", + "react-validatorjs-strategy": "^0.1.4", "swig": "^1.4.2", "validatorjs": "^2.0.2" }, diff --git a/server.js b/server.js index fe23e19f..c8eced52 100644 --- a/server.js +++ b/server.js @@ -15,13 +15,12 @@ var path = require('path'); var express = require('express'); var bodyParser = require('body-parser'); var swig = require('swig'); +var strategy = require('react-validatorjs-strategy'); +var schemas = require('./components/schemas'); require('node-jsx').install(); var components = require('./components/server'); -var strategy = require('./components/strategy'); -var schemas = require('./components/schemas'); - var app = express(); app.engine('html', swig.renderFile);