commit baf9395cadaeeb4eb76470049f8f454709a040bf
parent 1493b0e8bbc4d997d284d6a3cecd1142e29389b5
Author: Konstantin Tarkus <hello@tarkus.me>
Date: Tue, 11 Nov 2014 21:30:46 +0300
Add PageActions, PageStore; specify layouts by using static properties
Diffstat:
16 files changed, 376 insertions(+), 62 deletions(-)
diff --git a/gulpfile.js b/gulpfile.js
@@ -1,6 +1,9 @@
-/*!
- * Facebook React Starter Kit | https://github.com/kriasoft/react-starter-kit
- * Copyright (c) KriaSoft, LLC. All rights reserved. See LICENSE.txt
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
*/
'use strict';
@@ -16,6 +19,9 @@ var runSequence = require('run-sequence');
var webpack = require('webpack');
var browserSync = require('browser-sync');
var pagespeed = require('psi');
+var fs = require('fs');
+var url = require('url');
+var ReactTools = require('react-tools');
var argv = require('minimist')(process.argv.slice(2));
// Settings
@@ -47,6 +53,20 @@ var pkgs = (function() {
return pkgs;
}());
+// Configure JSX Harmony transform in order to be able
+// require .js files with JSX (see 'pages' task)
+var originalJsTransform = require.extensions['.js'];
+var reactTransform = function(module, filename) {
+ if (filename.indexOf('node_modules') === -1) {
+ var src = fs.readFileSync(filename, {encoding: 'utf8'});
+ src = ReactTools.transform(src, {harmony: true});
+ module._compile(src, filename);
+ } else {
+ originalJsTransform(module, filename);
+ }
+};
+require.extensions['.js'] = reactTransform;
+
// The default task
gulp.task('default', ['serve']);
@@ -88,8 +108,28 @@ gulp.task('images', function() {
// HTML pages
gulp.task('pages', function() {
src.pages = ['src/pages/**/*.js', 'src/pages/404.html'];
- var render = $.render({template: './src/pages/_template.html'})
+
+ var data = {};
+ var ActionTypes = require('./src/constants/ActionTypes');
+ var AppDispatcher = require('./src/AppDispatcher');
+
+ // Capture document.title changes
+ AppDispatcher.register(function(payload) {
+ switch (payload.action.actionType)
+ {
+ case ActionTypes.SET_PAGE_TITLE:
+ data.title = payload.action.text;
+ break;
+ }
+ return true;
+ });
+
+ var render = $.render({
+ template: './src/pages/_template.html',
+ data: function() { return data; }
+ })
.on('error', function(err) { console.log(err); render.end(); });
+
return gulp.src(src.pages)
.pipe($.changed(DEST, {extension: '.html'}))
.pipe($.if('*.js', render))
@@ -154,8 +194,6 @@ gulp.task('build', ['clean'], function(cb) {
// Launch a lightweight HTTP Server
gulp.task('serve', function(cb) {
- var url = require('url');
- var fs = require('fs');
watch = true;
runSequence('build', function() {
diff --git a/package.json b/package.json
@@ -30,7 +30,7 @@
"gulp-load-plugins": "^0.7.1",
"gulp-minify-css": "^0.3.11",
"gulp-plumber": "^0.6.6",
- "gulp-render": "^0.1.1",
+ "gulp-render": "^0.2.0",
"gulp-replace": "^0.5.0",
"gulp-size": "^1.1.0",
"gulp-uglify": "^1.0.1",
diff --git a/src/AppDispatcher.js b/src/AppDispatcher.js
@@ -1,6 +1,9 @@
/*
- * A singleton that operates as the central hub for application updates.
- * For more information visit https://facebook.github.io/flux/
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
*/
'use strict';
@@ -9,6 +12,10 @@ var {Dispatcher} = require('flux');
var PayloadSources = require('./constants/PayloadSources');
var assign = require('object-assign');
+/**
+ * A singleton that operates as the central hub for application updates.
+ * For more information visit https://facebook.github.io/flux/
+ */
var AppDispatcher = assign(new Dispatcher(), {
/**
@@ -18,7 +25,7 @@ var AppDispatcher = assign(new Dispatcher(), {
handleServerAction(action) {
var payload = {
source: PayloadSources.SERVER_ACTION,
- action
+ action: action
};
this.dispatch(payload);
},
@@ -30,7 +37,7 @@ var AppDispatcher = assign(new Dispatcher(), {
handleViewAction(action) {
var payload = {
source: PayloadSources.VIEW_ACTION,
- action
+ action: action
};
this.dispatch(payload);
}
diff --git a/src/actions/PageActions.js b/src/actions/PageActions.js
@@ -0,0 +1,51 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+'use strict';
+
+var AppDispatcher = require('../AppDispatcher');
+var ActionTypes = require('../constants/ActionTypes');
+
+var PageActions = {
+
+ /**
+ * Set a title for the current page.
+ * @param {string} text The text to be set as a page title.
+ */
+ setTitle(text) {
+ AppDispatcher.handleViewAction({
+ actionType: ActionTypes.SET_PAGE_TITLE,
+ text: text
+ });
+ },
+
+ /**
+ * Set description for the current page.
+ * @param {string} text The text to be set as a page description.
+ */
+ setDescription(text) {
+ AppDispatcher.handleViewAction({
+ actionType: ActionTypes.SET_PAGE_DESC,
+ text: text
+ });
+ },
+
+ /**
+ * Set keywords for the current page.
+ * @param {string} text The text to be set as page keywords.
+ */
+ setKeywords(text) {
+ AppDispatcher.handleViewAction({
+ actionType: ActionTypes.SET_PAGE_KEYWORDS,
+ text: text
+ });
+ }
+
+};
+
+module.exports = PageActions;
diff --git a/src/actions/RouteActions.js b/src/actions/RouteActions.js
@@ -1,3 +1,11 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
'use strict';
var AppDispatcher = require('../AppDispatcher');
@@ -12,7 +20,7 @@ var AppActions = {
setRoute(route) {
AppDispatcher.handleViewAction({
actionType: ActionTypes.SET_CURRENT_ROUTE,
- route
+ route: route
});
}
diff --git a/src/app.js b/src/app.js
@@ -1,32 +1,54 @@
-/*!
- * Facebook React Starter Kit | https://github.com/kriasoft/react-starter-kit
- * Copyright (c) KriaSoft, LLC. All rights reserved. See LICENSE.txt
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
*/
'use strict';
var React = require('react');
+var ExecutionEnvironment = require('react/lib/ExecutionEnvironment');
var {Router} = require('director');
var AppDispatcher = require('./AppDispatcher');
var ActionTypes = require('./constants/ActionTypes');
-var assign = require('object-assign');
+var router;
// Export React so the dev tools can find it
(window !== window.top ? window.top : window).React = React;
+AppDispatcher.register((payload) => {
+
+ var action = payload.action;
+
+ switch (action.actionType)
+ {
+ case ActionTypes.SET_CURRENT_ROUTE:
+ router.setRoute(action.route);
+ break;
+
+ case ActionTypes.SET_PAGE_TITLE:
+ if (ExecutionEnvironment.canUseDOM) {
+ document.title = action.text;
+ }
+ break;
+ }
+
+ return true; // No errors. Needed by promise in Dispatcher.
+});
+
/**
* Check if Page component has a layout property; and if yes, wrap the page
* into the specified layout, then mount to document.body.
*/
function render(page) {
- var child = null, props = {};
- while (page.defaultProps && page.defaultProps.layout) {
+ var layout = null, child = null, props = {};
+ while ((layout = page.type.layout || (page.defaultProps && page.defaultProps.layout))) {
child = React.createElement(page, props, child);
- assign(props, page.defaultProps);
- page = page.defaultProps.layout;
+ page = layout;
}
React.render(React.createElement(page, props, child), document.body);
- document.title = props.title;
}
// Define URL routes
@@ -37,15 +59,4 @@ var routes = {
};
// Initialize a router
-var router = new Router(routes).configure({html5history: true}).init();
-
-AppDispatcher.register((payload) => {
-
- var action = payload.action;
-
- if (action.actionType === ActionTypes.SET_CURRENT_ROUTE) {
- router.setRoute(action.route);
- }
-
- return true; // No errors. Needed by promise in Dispatcher.
-});
+router = new Router(routes).configure({html5history: true}).init();
diff --git a/src/components/Link.js b/src/components/Link.js
@@ -1,3 +1,11 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
'use strict';
var React = require('react');
diff --git a/src/components/Navbar.js b/src/components/Navbar.js
@@ -1,3 +1,11 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
'use strict';
var React = require('react');
diff --git a/src/components/__tests__/Navbar-test.js b/src/components/__tests__/Navbar-test.js
@@ -1,3 +1,11 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
/* global jest, describe, it, expect */
'use strict';
diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js
@@ -1,6 +1,19 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
module.exports = {
// Route action types
- SET_CURRENT_ROUTE: 'SET_CURRENT_ROUTE'
+ SET_CURRENT_ROUTE: 'SET_CURRENT_ROUTE',
+
+ // Page action types
+ SET_PAGE_TITLE: 'SET_PAGE_TITLE',
+ SET_PAGE_DESC: 'SET_PAGE_DESC',
+ SET_PAGE_KEYWORDS: 'SET_PAGE_KEYWORDS'
};
diff --git a/src/constants/PayloadSources.js b/src/constants/PayloadSources.js
@@ -1,4 +1,14 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
module.exports = {
+
VIEW_ACTION: 'VIEW_ACTION',
SERVER_ACTION: 'SERVER_ACTION'
+
};
diff --git a/src/constants/Settings.js b/src/constants/Settings.js
@@ -0,0 +1,15 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+module.exports = {
+
+ title: 'React.js Starter Kit',
+ description: 'A skeleton for an isomorphic web application (SPA) built with React.js and Flux',
+ keywords: null
+
+};
diff --git a/src/layouts/DefaultLayout.js b/src/layouts/DefaultLayout.js
@@ -1,25 +1,48 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
'use strict';
var React = require('react');
+var PageStore = require('../stores/PageStore');
var Link = require('../components/Link');
var Navbar = require('../components/Navbar');
+/**
+ * Retrieve the current page metadata from the PageStore.
+ * @returns {{title: string}}
+ */
+function getState() {
+ return {
+ title: PageStore.getCurrentPage().title
+ };
+}
+
var DefaultLayout = React.createClass({
- propTypes: {
- title: React.PropTypes.string,
- breadcrumb: React.PropTypes.element
+
+ getInitialState() {
+ return getState();
},
- getDefaultProps() {
- return {
- title: 'React.js Starter Kit',
- description: 'A skeleton for an isomorphic web application (SPA) built with React.js and Flux'
- };
+
+ componentDidMount() {
+ PageStore.addEventListener(this._onChange);
+ this._onChange();
},
+
+ componentWillUnmount() {
+ PageStore.removeEventListener(this._onChange);
+ },
+
render() {
- var header = this.props.breadcrumb ? (
+ var header = this.props.children.type.breadcrumb ? (
<div className="container">
- <h2>{this.props.title}</h2>
- {this.props.breadcrumb}
+ <h2>{this.state.title}</h2>
+ {this.props.children.type.breadcrumb}
</div>
) : (
<div className="jumbotron">
@@ -46,6 +69,13 @@ var DefaultLayout = React.createClass({
</div>
</div>
);
+ },
+
+ /**
+ * Event handler for 'change' events coming from the PageStore
+ */
+ _onChange() {
+ this.setState(getState());
}
});
diff --git a/src/pages/Index.js b/src/pages/Index.js
@@ -1,15 +1,27 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
'use strict';
var React = require('react');
+var PageActions = require('../actions/PageActions');
var DefaultLayout = require('../layouts/DefaultLayout');
var HomePage = React.createClass({
- getDefaultProps() {
- return {
- title: 'React.js Starter Kit',
- layout: DefaultLayout
- };
+
+ statics: {
+ layout: DefaultLayout
+ },
+
+ componentWillMount() {
+ PageActions.setTitle('React.js Starter Kit');
},
+
render() {
return (
<div className="container">
@@ -44,6 +56,7 @@ var HomePage = React.createClass({
</div>
);
}
+
});
module.exports = HomePage;
diff --git a/src/pages/Privacy.js b/src/pages/Privacy.js
@@ -1,22 +1,34 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
'use strict';
var React = require('react');
-var Link = require('../components/Link');
+var PageActions = require('../actions/PageActions');
var DefaultLayout = require('../layouts/DefaultLayout');
+var Link = require('../components/Link');
var PrivacyPage = React.createClass({
- getDefaultProps() {
- return {
- title: 'Privacy Policy',
- layout: DefaultLayout,
- breadcrumb: (
- <ol className="breadcrumb">
- <li><Link to="/">Home</Link></li>
- <li className="active">Privacy</li>
- </ol>
- )
- };
+
+ statics: {
+ layout: DefaultLayout,
+ breadcrumb: (
+ <ol className="breadcrumb">
+ <li><Link to="/">Home</Link></li>
+ <li className="active">Privacy</li>
+ </ol>
+ )
},
+
+ componentWillMount() {
+ PageActions.setTitle('Privacy Policy');
+ },
+
render() {
return (
<div className="container">
@@ -63,6 +75,7 @@ var PrivacyPage = React.createClass({
</div>
);
}
+
});
module.exports = PrivacyPage;
diff --git a/src/stores/PageStore.js b/src/stores/PageStore.js
@@ -0,0 +1,81 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+'use strict';
+
+var AppDispatcher = require('../AppDispatcher');
+var ActionTypes = require('../constants/ActionTypes');
+var Settings = require('../constants/Settings');
+var EventEmitter = require('events').EventEmitter;
+var assign = require('object-assign');
+
+var CHANGE_EVENT = 'change';
+
+var page = {
+ title: null,
+ description: null,
+ keywords: null
+};
+
+var PageStore = assign({}, EventEmitter.prototype, {
+
+ /**
+ * Get metadata associated with the current page.
+ * @returns {object}
+ */
+ getCurrentPage() {
+ return {
+ title: page.title || Settings.title,
+ description: page.description || Settings.description,
+ keywords: page.keywords || Settings.keywords
+ };
+ },
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ },
+
+ addEventListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ },
+
+ removeEventListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+});
+
+
+AppDispatcher.register(function(payload) {
+
+ var action = payload.action;
+
+ switch (action.actionType)
+ {
+ case ActionTypes.SET_PAGE_TITLE:
+ page.title = action.text;
+ break;
+
+ case ActionTypes.SET_PAGE_DESC:
+ page.description = action.description;
+ break;
+
+ case ActionTypes.SET_PAGE_KEYWORDS:
+ page.keywords = action.keywords;
+ break;
+
+ default:
+ return true;
+ }
+
+ PageStore.emitChange();
+
+ return true; // No errors. Needed by promise in Dispatcher.
+});
+
+module.exports = PageStore;