From b813eefbf738807ebe602e3d23a20e6d5aee4b1d Mon Sep 17 00:00:00 2001 From: Matthew Dressman Date: Wed, 11 Mar 2015 16:03:01 -0700 Subject: [PATCH] Add so much documentation --- README.md | 2 + package.json | 1 - server/index.js | 92 +++++++++++++++--------- server/services/project-service.js | 26 +++++-- src/actions/create-project.js | 9 +++ src/actions/load-home-page.js | 23 ++++-- src/app.js | 24 +++---- src/client.js | 33 +++++++-- src/components/html-component.jsx | 12 ++-- src/components/projects/project-form.jsx | 21 +++++- src/components/projects/project-list.jsx | 20 +++++- src/pages/homepage/home-page.jsx | 4 ++ src/routes.js | 3 + src/stores/project-store.js | 19 +++++ 14 files changed, 211 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index b599b09..b4679b7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ npm install gulp ``` +Visit [http://localhost:9312](http://localhost:9312) + ## Libraries - [fluxible](http://fluxible.io) - pluggable application container to facilitate an isomorphic React+Flux architecture. Developed by Yahoo. Makes use of plugins wrapping smaller core libraries: - [dispatchr](https://github.com/yahoo/dispatchr) - isolates dispatcher and stores per request diff --git a/package.json b/package.json index a94f660..88c85f1 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "dependencies": { "body-parser": "^1.10.0", "browserify": "^9.0.3", - "debug": "^2.1.1", "express": "^4.9.5", "flux-router-component": "^0.5.8", "fluxible": "^0.2.1", diff --git a/server/index.js b/server/index.js index ac396ce..974b811 100644 --- a/server/index.js +++ b/server/index.js @@ -2,81 +2,109 @@ require('node-jsx').install({extension: '.jsx', harmony: true}); // parses JSX and enables ES6/7 transpiling -var express = require('express'); -var bodyParser = require('body-parser'); -var app = require('../src/app'); // Fluxible app -var path = require('path'); // path util -var React = require('react'); +var express = require('express'); +var bodyParser = require('body-parser'); +var path = require('path'); var serialize = require('serialize-javascript'); -var debug = require('debug')('fluxMiddleware'); +var React = require('react'); +var app = require('../src/app'); // Fluxible app var navigateAction = require('flux-router-component').navigateAction; var HtmlComponent = React.createFactory(require('../src/components/html-component.jsx')); -// Make our node process bulletproof -process.on('uncaughtException', function(err) { - console.error(err.stack); -}); - -// initialize our server var server = express(); -server.set('state namespace', 'App'); - -// serve our 'build' dir assets under '/v2/assets' -server.use('/v2/assets', express.static(path.join(__dirname, '..', 'dist'))); - -// automatically parse any encoding of JSON server.use(bodyParser.json()); +server.use('/v2/assets', express.static(path.join(__dirname, '..', 'dist'))); -// Get access to the fetchr plugin instance +// Get access to the app's fetchr plugin instance var fetchrPlugin = app.getPlugin('FetchrPlugin'); -// Register our REST services +// Register our REST services with fetchr fetchrPlugin.registerService(require('./services/project-service')); -// Set up the fetchr middleware +// Set up the fetchr server middleware server.use(fetchrPlugin.getXhrPath(), fetchrPlugin.getMiddleware()); -// attach our flux middleware (this is our actual app) +/* + * The is the entrypoint into the Flux flow + */ server.use(function(req, res) { + /* + * Create a request-scoped context to isolate data per request + */ var context = app.createContext({ - req: req, // The fetchr plugin depends on this - xhrContext: { // Used as query params for all XHR calls - lang: 'en-US', // make sure XHR calls receive the same lang as the initial request - _csrf: 'a3fc2d' // CSRF token to validate on the server using your favorite library + req: req, // pass the request object to fetchr + xhrContext: { // query params for all fetchr XHR calls + lang: 'en-US', + _csrf: 'a3fc2d' } }); + /* + * Fluxible has context interfaces for Action Creators, Components, + * and Stores which provide access to Flux methods needed by each. + * + * Action Context provides dispatch, executeAction, and getStore + */ var actionContext = context.getActionContext(); - debug('Executing navigate action'); + /* + * navigateAction takes req.url and matches a route from src/routes.js, + * executing the route object's action, if it exists. After the route's + * action, we continue in this function's callback. + */ actionContext.executeAction(navigateAction, { url: req.url }, function (err) { + if (err) { res.status(500); res.send("five hundo. sad panda."); return; } - debug('Exposing context state to client as window.App'); + /* + * Exposing your app's server-rendered state so React can re-initialize + * client-side on top of the existing DOM. + * + * Dispatchr provides dehydrate/rehydrayte functions that will serialize + * data from all registered stores. + * + * We create a string variable to pass into the HtmlComponent below, + * which will be rendered as JavaScript to create an App variable on + * the global window object. + */ var exposed = 'window.App=' + serialize(app.dehydrate(context)) + ';'; - debug('Rendering Application component into html'); + // Application's root component defined in app.js var Component = app.getComponent(); + /* + * Render our React application to basic HTML. This function adds + * data-reactid attributes to each DOM node for the client to reconcile. + * + * Component Context provides access to getStore and executeAction and + * is shared with all child components using React's built-in context + * thanks to FluxibleMixin. + * + * The markup prop will contain the output from React rendering our + * root app component. + * + */ var html = React.renderToStaticMarkup(HtmlComponent({ context: context.getComponentContext(), state: exposed, - markup: React.renderToString(Component({context:context.getComponentContext()})) + markup: React.renderToString( + Component({ context: context.getComponentContext() }) + ) })); - debug('Sending markup'); + // Render! res.send(html); }); }); -// ok, ready: run the server +// ok, run the server var port = 9312; server.listen(port, function() { var env = process.env.NODE_ENV || 'development'; diff --git a/server/services/project-service.js b/server/services/project-service.js index ebbe5aa..6264848 100644 --- a/server/services/project-service.js +++ b/server/services/project-service.js @@ -2,17 +2,23 @@ var serverData = require('./data/project-data'); -/** - * Returns project data +/* + * Fetchr provides an abstraction which allows you to fetch data using the same + * syntax on both server and client. For example, you can make a request using + * node's request module on the server and use the same service service on the + * client without having to write a separate AJAX request to the same endpoint. * - * @param {Object} config (optional) - * @param {function} callback async callback - * - * @return {Object} list of projects + * This example service demonstrates asynchronous service by reading and + * writing to an array in a setTimeout call. */ module.exports = { + + // Actions use this name to call fetchr services name: 'projectService', + /* + * Fetchr requires CRUD interface names + */ read: function(req, resource, params, config, callback) { setTimeout(function () { callback(null, JSON.parse(JSON.stringify(serverData))); @@ -28,5 +34,11 @@ module.exports = { callback(null, serverData); }, 10); } - + /* + * Exercise! + * - Add Delete project feature + * + * update: function(req, resource, params, body, config, callback) {}, + * delete: function(req, resource, params, config, callback) {} + */ }; \ No newline at end of file diff --git a/src/actions/create-project.js b/src/actions/create-project.js index 2fea7c8..9d23840 100644 --- a/src/actions/create-project.js +++ b/src/actions/create-project.js @@ -2,6 +2,10 @@ module.exports = function (context, payload, done) { + /* + * Calls the service's create function and passes in data. This service + * returns all projects including the one newly added. + */ context.service.create('projectService', payload, {}, function(err, projects) { if (err) { console.error(err); @@ -9,6 +13,11 @@ module.exports = function (context, payload, done) { return; } + /* + * Dispatches the same event as the server's load action, + * passing along an updated list of projects to all stores registered + * to handle this event. + */ context.dispatch('RECEIVE_PROJECTS_SUCCESS', projects); done(); }); diff --git a/src/actions/load-home-page.js b/src/actions/load-home-page.js index a32b977..5af0823 100644 --- a/src/actions/load-home-page.js +++ b/src/actions/load-home-page.js @@ -2,23 +2,34 @@ var RSVP = require('rsvp'); -/** - * executes the navigation to the home page - * - * @param {Context} context - * @param {Object} payload: the route object - * @param {Fn} done +/* + * Action executed on the server before route loads. All server functionality, + * data fetching, etc. needed for initial page load happens here. */ module.exports = function (context, payload, done) { + // Create RSVP promise to fetch asynchronous data from the server var getProjects = new RSVP.Promise(function (resolve, reject) { + + /* + * Call projectService's read() with fetchr and handle the response + * in callback. If the call succeeded, resolve the promise passing in + * the server payload. Otherwise, reject the promise with the error. + */ context.service.read('projectService', {}, {}, function(err, results) { if (err) { reject(err); } resolve(results); }); + }); getProjects.then(function(projects) { + /* + * Flux magic! + * + * Dispatch a named event to all stores registered to handle this + * specific event, passing along the action's payload. + */ context.dispatch('RECEIVE_PROJECTS_SUCCESS', projects); done(); }).catch(function (err) { diff --git a/src/app.js b/src/app.js index 85f36a0..15824b7 100644 --- a/src/app.js +++ b/src/app.js @@ -1,26 +1,26 @@ 'use strict'; var React = require('react'); -var FluxibleApp = require('fluxible'); +var Fluxible = require('fluxible'); var fetchrPlugin = require('fluxible-plugin-fetchr'); var routrPlugin = require('fluxible-plugin-routr'); /* -* This is our porch global flux app. It registers all of our pages, stores, -* and handles the basic routing and page loading. -*/ -var app = new FluxibleApp({ + * Common application setup code. + * + * - Create new Fluxible app instance + * - Define root application component + * - Install plugins + * - Register stores + */ + +var app = new Fluxible({ component: React.createFactory(require('./pages/homepage/home-page')) }); -app.plug(fetchrPlugin({ - xhrPath: '/napi/' -})); +app.plug(fetchrPlugin()); -app.plug(routrPlugin({ - routes: require('./routes') -})); +app.plug(routrPlugin({ routes: require('./routes') })); -// Register required stores app.registerStore(require('../src/stores/project-store')); module.exports = app; diff --git a/src/client.js b/src/client.js index a7b2d18..65f3249 100644 --- a/src/client.js +++ b/src/client.js @@ -1,20 +1,39 @@ 'use strict'; var app = require('./app'); - var React = require('react'); -var dehydratedState = window.App; // sent from the server -window.React = React; // for chrome dev tool support +window.React = React; // for Chrome DevTools support + +/* + * Grab dehydrated application state from all stores. + * Sent from the server + */ +var dehydratedState = window.App; +/* + * Re-initialize application state and provides the request's + * context object to the callback + */ app.rehydrate(dehydratedState, function (err, context) { if (err) { throw err; } - window.context = context; + window.context = context; var mountNode = document.getElementById('app'); + var Component = app.getComponent(); - React.render(app.getComponent()({context: context.getComponentContext()}), mountNode, function () { - console.log('React client-side rendered.'); - }); + /* + * React will "render" the application component at the mountNode and + * compare the results with the existing server-rendered DOM. + * If everything matches (!!), React will mount itself on top and attach + * client-side event handlers. + */ + React.render( + Component({context: context.getComponentContext()}), + mountNode, + function () { + console.log('React client-side rendered.'); + } + ); }); \ No newline at end of file diff --git a/src/components/html-component.jsx b/src/components/html-component.jsx index 575bd0b..aa9014b 100644 --- a/src/components/html-component.jsx +++ b/src/components/html-component.jsx @@ -1,16 +1,12 @@ "use strict"; -var React = require('react/addons'); -var FluxibleMixin = require('fluxible').Mixin; +var React = require('react'); +var FluxibleMixin = require('fluxible').Mixin; var Html = React.createClass({ mixins: [ FluxibleMixin ], - getInitialState: function () { - return {}; - }, - render: function () { return ( @@ -26,12 +22,14 @@ var Html = React.createClass({ + {/* Inject root application component */}
- {/* dehydrated json state of all the stores */} + {/* Exposes dehydrated json state of all the stores as window.App */} + {/* Generated JavaScript from gulp browserify. Loads client.js */} ); diff --git a/src/components/projects/project-form.jsx b/src/components/projects/project-form.jsx index 716b119..fb80d1a 100644 --- a/src/components/projects/project-form.jsx +++ b/src/components/projects/project-form.jsx @@ -9,6 +9,10 @@ var ProjectForm = React.createClass({ mixins: [FluxibleMixin], getInitialState: function() { + /* + * The form below uses a controlled ReactElement, which means the + * input's value is set by the component's state. + */ return { value: '' }; @@ -17,17 +21,27 @@ var ProjectForm = React.createClass({ submitForm: function (e) { e.preventDefault(); - // TODO: Add form validation assignment w instructions + /* + * Exercise! + * - Add form validation functionality and return (alert, etc.) an error + */ var formData = { projectName: this.state.value, projectImg: "http://placehold.it/546x408" }; + // Executes the createProject action and passes along the form data this.executeAction(createProject, formData); + + // Resets the form input this.setState({ value: '' }); }, + /* + * Called on every change to the form's controlled input and updates the + * component's local state + */ handleChange: function(e) { e.preventDefault(); this.setState({ value: e.target.value }); @@ -46,8 +60,9 @@ var ProjectForm = React.createClass({ value={this.state.value} onChange={this.handleChange} /> - diff --git a/src/components/projects/project-list.jsx b/src/components/projects/project-list.jsx index dbf88b7..300bc3a 100644 --- a/src/components/projects/project-list.jsx +++ b/src/components/projects/project-list.jsx @@ -9,6 +9,10 @@ var ProjectList = React.createClass({ mixins: [ FluxibleMixin ], + /* + * Whenever this component hears a change emitted from a store it is + * listening to, it will execute the onChange handler function. + */ statics: { storeListeners: [ ProjectStore ] }, @@ -17,20 +21,27 @@ var ProjectList = React.createClass({ return this.getStateFromStores(); }, + /* + * Grab the current state of data from our stores + */ getStateFromStores: function () { return { projects: this.getStore(ProjectStore).getProjects() }; }, + /* + * Flux magic! + * + * A store emitted a change! Update the component's state with current + * store data, which will trigger a re-render. + */ onChange: function() { this.setState(this.getStateFromStores()); }, render: function () { - // TODO: Add Delete project assignment - return (
@@ -59,7 +70,10 @@ var Project = React.createClass({ var p = this.props.project; - // TODO: Add Read More assignment using state and classSet + /* + * Exercise! + * - Add Read More functionality using state and classSet + */ return (
diff --git a/src/pages/homepage/home-page.jsx b/src/pages/homepage/home-page.jsx index 0f339df..27534e3 100644 --- a/src/pages/homepage/home-page.jsx +++ b/src/pages/homepage/home-page.jsx @@ -6,6 +6,10 @@ var ProjectList = require('../../components/projects/project-list'); var ProjectForm = require('../../components/projects/project-form'); var Footer = require('../../components/footer'); +/* + * Root application component, defined in src/app.js, + * renders child components which is where the magic happens. + */ var HomePage = React.createClass({ mixins: [ FluxibleMixin ], diff --git a/src/routes.js b/src/routes.js index b7eeb16..3380ba1 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,5 +1,8 @@ "use strict"; +/* + * Route's action will be executed by navigateAction in server/index + */ module.exports = { HomePage: { diff --git a/src/stores/project-store.js b/src/stores/project-store.js index b614876..96655ff 100644 --- a/src/stores/project-store.js +++ b/src/stores/project-store.js @@ -5,6 +5,12 @@ var createStore = require('fluxible/utils/createStore'); var ProjectStore = createStore({ storeName: "ProjectStore", + /* + * Whenever one of these events is dispatched from an action, handle it with + * the corresponding function. Note: multiple stores can listen to the same + * event, and Fluxible provides a waitFor function to handle inter-store + * dependencies. + */ handlers: { 'RECEIVE_PROJECTS_SUCCESS': 'updateProjects' }, @@ -17,11 +23,24 @@ var ProjectStore = createStore({ return this.projects; }, + /* + * Update the store's internal state with the payload from an action. + * + * Flux magic! + * this.emitChange() will send a change event to all components who are + * listening for updates from this particular store. When a subscribed + * component hears this emitted change, it will trigger a re-render and + * React happiness follows. + */ updateProjects: function (payload) { this.projects = payload; this.emitChange(); }, + /* + * Dehydrate/rehydrate used to share store's state from server to client. + * More explanation in server/index + */ dehydrate: function () { return { projects: this.projects