Skip to content

onDispatch callback and connect middleware #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: 1.x
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"asi": true,
"expr": true,
"globalstrict": true,
"eqnull": true,
"globals": {
"window": false,
"Buffer": false,
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ lint:
test: test-unit test-server

test-unit:
@mocha -R spec -b tests/matchRoutes.js
@mocha -R spec -b tests/matchRoutes.js tests/getContentMeta.js

test-server:
@mocha -R spec -b tests/server.js
Expand Down
1 change: 0 additions & 1 deletion lib/Router.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use strict";

var React = require('react');
var merge = require('react/lib/merge');
var Preloaded = require('react-async/lib/Preloaded');
var RoutingEnvironmentMixin = require('./RoutingEnvironmentMixin');
var matchRoutes = require('./matchRoutes');
Expand Down
22 changes: 19 additions & 3 deletions lib/RoutingEnvironmentMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
var React = require('react');
var merge = require('react/lib/merge');
var Environment = require('./environment');
var invariant = require('react/lib/invariant');

/**
* Mixin which makes router bound to an environment. This is the glue that binds
Expand All @@ -28,7 +27,8 @@ var RoutingEnvironmentMixin = {
hash: React.PropTypes.bool,

onBeforeNavigation: React.PropTypes.func,
onNavigation: React.PropTypes.func
onNavigation: React.PropTypes.func,
onDispatch: React.PropTypes.func
},

childContextTypes: {
Expand Down Expand Up @@ -166,6 +166,17 @@ var RoutingEnvironmentMixin = {
return join(this.getPrefix(), href);
},

componentWillMount: function() {
if (this.props.onDispatch) {
var path = this.getPath();
this.props.onDispatch(path, {
initial: true,
isPopState: false,
match: this.match(path)
});
}
},

componentDidMount: function() {
if (this.isControlled()) {
return;
Expand Down Expand Up @@ -226,9 +237,14 @@ EnvironmentListener.prototype.onBeforeNavigation = function(path, navigation) {
}

EnvironmentListener.prototype.onNavigation = function(path, navigation) {
var result;
if (this.router.props.onNavigation) {
return this.router.props.onNavigation(path, navigation);
result = this.router.props.onNavigation(path, navigation);
}
if (this.router.props.onDispatch) {
this.router.props.onDispatch(path, navigation);
}
return result;
}

module.exports = RoutingEnvironmentMixin;
86 changes: 86 additions & 0 deletions lib/contrib/connect-middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use strict";

var parseUrl = require('url').parse;
var React = require('react');
var ReactAsync = require('react-async');
var merge = require('react/lib/merge');
var invariant = require('react/lib/invariant');
var getContentMeta = require('../getContentMeta');


/**
* A connect middleware for server-side rendering of React Router components.
* Typically, these are instances of Router, but the only requirements are that
* the component accepts "path" on "onDispatch" props.
*/
function reactRouter(Router, options) {
invariant(
React.isValidClass(Router),
'Argument must be a React component class but got: %s', Router
);
var meta = getContentMeta(options && options.doctype,
options && options.contentType);
var props = options && options.props || {};
return function(req, res, next) {
var pathname = parseUrl(req.url).pathname;
if (pathname == null) {
next(new Error('Invalid URL: ' + req.url));
return;
}

// This callback is meant to be called multiple times with whatever
// information is available. It will aggregate the information and send a
// response or emit an error as soon as is possible.
var memo = {};
var callback = function(err, statusCode, markup) {
if (memo.complete) {
return; // Don't take action more than once.
}
if (err != null) {
// If we errored, we're done.
memo.complete = true;
next(err);
return;
}

// Remember the status code and markup (since we may not get them at the
// same time).
if (statusCode != null) {
memo.statusCode = statusCode;
}
if (markup != null) {
memo.markup = markup;
}

// Wait for the render to complete and onDispatch to be invoked.
if (memo.markup == null || memo.statusCode == null) {
return;
}
memo.complete = true;
res.statusCode = memo.statusCode;
res.setHeader('Content-Type', meta.contentType);
res.end('' + meta.doctype + memo.markup);
};

try {
var app = Router(merge(props, {
path: pathname,
onDispatch: function(path, navigation) {
var statusCode = (navigation && navigation.match &&
navigation.match.isNotFound ? 404 : 200);
callback(null, statusCode);
}
}));
ReactAsync.renderComponentToStringWithAsyncState(
app,
function(err, markup) {
callback(err, null, markup);
}
);
} catch (err) {
next(err);
}
};
}

module.exports = reactRouter;
68 changes: 68 additions & 0 deletions lib/getContentMeta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use strict";

var doctypes = {
'text/html': {
'<!DOCTYPE html>': ['default', '5', 'html', 'html5'],
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">': ['4', 'html4']
},
'application/xhtml+xml': {
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">': ['strict'],
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">': ['transitional'],
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">': ['frameset'],
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">': ['1.1'],
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">': ['basic'],
'<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">': ['mobile']
},
'image/svg+xml': {
'': ['svg'] // https://jwatt.org/svg/authoring/#doctype-declaration
}
};


function getContentMeta(doctype, contentType) {
var ct, dt, doctypesToShortcuts, shortcuts;

if (doctype == null && contentType == null) {
doctype = 'default';
}

// Look up the content type
if (doctype) {
for (ct in doctypes) {
doctypesToShortcuts = doctypes[ct];
if (!doctypesToShortcuts) { continue; }

for (dt in doctypesToShortcuts) {
shortcuts = doctypesToShortcuts[dt];
if (!shortcuts) { continue; }
if (String(doctype).toLowerCase() === dt.toLowerCase() || shortcuts.indexOf(String(doctype).toLowerCase()) > -1) {
return {doctype: dt, contentType: contentType == null ? ct: contentType};
}
}
}
}

if (contentType == null) {
throw new Error('Could not find matching content type for doctype: ' + doctype);
}

// Look up the doctype.
for (ct in doctypes) {
if (contentType.toLowerCase() === ct.toLowerCase()) {
doctypesToShortcuts = doctypes[ct];
if (!doctypesToShortcuts) { continue; }
for (dt in doctypesToShortcuts) {
return {doctype: dt, contentType: contentType};
}
}
}

if (doctype == null) {
throw new Error('Could not find matching doctype type for content type: ' + contentType);
}

return {doctype: doctype, contentType: contentType};
}


module.exports = getContentMeta;
11 changes: 8 additions & 3 deletions tests/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ describe('Routing', function() {
Router.Locations({
ref: 'router', className: 'App',
onNavigation: this.props.navigationHandler,
onBeforeNavigation: this.props.beforeNavigationHandler
onBeforeNavigation: this.props.beforeNavigationHandler,
onDispatch: this.props.dispatchHandler
},
Router.Location({
path: '/__zuul',
Expand Down Expand Up @@ -184,7 +185,7 @@ describe('Routing', function() {
});

describe('Navigation lifecycle callbacks', function () {
it('calls onBeforeNaviation and onNavigation', function(done) {
it('calls onBeforeNaviation, onNavigation, and onDispatch', function(done) {
assertRendered('mainpage');
var called = [];
app.setProps({
Expand All @@ -193,12 +194,16 @@ describe('Routing', function() {
},
navigationHandler: function () {
called.push('onNavigation');
},
dispatchHandler: function (path) {
called.push(path)
}
});
router.navigate('/__zuul/hello', function () {
assert.equal(called.length, 2);
assert.equal(called.length, 3);
assert.equal(called[0], '/__zuul/hello');
assert.equal(called[1], 'onNavigation');
assert.equal(called[2], '/__zuul/hello');
done();
});
});
Expand Down
109 changes: 109 additions & 0 deletions tests/getContentMeta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
var assert = require('assert');
var getContentMeta = require('../lib/getContentMeta');

describe('getContentMeta', function() {

it('finds doctype by content type', function() {
assert.strictEqual(
getContentMeta(null, 'text/html').doctype,
'<!DOCTYPE html>'
);
assert.strictEqual(
getContentMeta(null, 'application/xhtml+xml').doctype,
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
);
assert.strictEqual(
getContentMeta(null, 'image/svg+xml').doctype,
''
);
});

it('finds content type by full doctype', function() {
assert.strictEqual(
getContentMeta(
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
).contentType,
'text/html'
);
});

it("doesn't find a match for an empty doctype", function() { // Even though it matches SVG
assert.throws(
function() { getContentMeta(''); },
/Could not find matching content type/
);
});

it("doesn't find a match for an nonsense doctype", function() {
assert.throws(
function() { getContentMeta('missingno'); },
/Could not find matching content type/
);
});

it("doesn't find a match for an nonsense content type", function() {
assert.throws(
function() { getContentMeta(null, 'missingno'); },
/Could not find matching doctype/
);
});

it('finds content type by doctype nickname', function() {
assert.strictEqual(
getContentMeta(5).contentType,
'text/html'
);
assert.strictEqual(
getContentMeta('html5').contentType,
'text/html'
);
assert.strictEqual(
getContentMeta('svg').contentType,
'image/svg+xml'
);
});

it('finds actual doctype by doctype nickname', function() {
assert.strictEqual(
getContentMeta(4).doctype,
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
);
assert.strictEqual(
getContentMeta('svg').doctype,
''
);
assert.strictEqual(
getContentMeta(4, 'text/html').doctype,
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
);
assert.deepEqual(
getContentMeta('strict', 'text/html'),
{
doctype: '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
contentType: 'text/html'
}
);
});

it('defaults to html5', function() {
assert.deepEqual(
getContentMeta(),
{doctype: '<!DOCTYPE html>', contentType: 'text/html'}
);
});

it('defaults to html5', function() {
assert.deepEqual(
getContentMeta(),
{doctype: '<!DOCTYPE html>', contentType: 'text/html'}
);
});

it('passes through explicit values', function() {
assert.deepEqual(
getContentMeta('hello', 'goodbye'),
{doctype: 'hello', contentType: 'goodbye'}
);
});

});
Loading