├── .babelrc ├── .gitignore ├── Procfile ├── README.md ├── app.js ├── app.json ├── bin └── www ├── ngrok.png ├── package.json ├── public ├── bundle.js ├── index.html ├── package.json ├── src │ ├── actions │ │ └── index.js │ ├── api │ │ └── index.js │ ├── components │ │ ├── PostDetails.js │ │ ├── PostsForm.js │ │ ├── PostsList.js │ │ └── header.js │ ├── containers │ │ ├── HeaderContainer.js │ │ ├── PostDetailsContainer.js │ │ ├── PostFormContainer.js │ │ └── PostsListContainer.js │ ├── index.js │ ├── pages │ │ ├── App.js │ │ ├── PostsIndex.js │ │ ├── PostsNew.js │ │ └── PostsShow.js │ ├── reducers │ │ ├── index.js │ │ └── reducer_posts.js │ └── routes.js └── style │ └── style.css ├── routes ├── index.js └── posts.js ├── sample-vf-page.vfp ├── views └── error.html └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /blog-server/node_modules 3 | /blog-client/node_modules 4 | npm-debug.log 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Redux CRUD App In Visualforce 2 | 3 | #Unmanaged Package 4 | 1. Install the unmanaged package: https://login.salesforce.com/packaging/installPackage.apexp?p0=04ti0000000YACJ 5 | 2. Go to: `/apex/reactreduxblog` to see the app. 6 | 7 | #Local Installation 8 | 1. Install Node.js 9 | 2. `git clone https://github.com/rajaraodv/react-redux-blog-vf.git` 10 | 3. `cd react-redux-blog-vf` 11 | 4. `npm install` 12 | 13 | #Install ngrok for localhost tunneling 14 | Install ngrok. It provides a way to serve/expose your localhost files to the internet even in **https (required by Visualforce)**. 15 | 16 | **This is great for VF development**. Because now, you can develop React Redux (or angular or whatever) locally and directly load the JS from within Visualforce while you are still developing it! So you won't have to upload the JS to Static Resource everytime you make changes to the code! 17 | 18 | For example: In your Visualforce, 19 | Instead of point to static resource, you can point to something that looks like below. Notice that `bundle.js` is actually the main app file that's currently being developed on localhost! 20 | 21 | `` 22 | 23 | **Note: Once you are all done, simply copy the final bundle.js from 'public' folder in your localhost (see production section) to static resource and update the url above to point to static resource.** 24 | 25 | #Custom Object 26 | Create a custom object "Post" with following three fields: 27 | 28 | 1. "Name" (This is the default/standard text field), 29 | 2. "Categories" (text) 30 | 3. "Content" (textarea) 31 | 32 | #Development: Local + Visualforce 33 | *You need two terminal windows open*, one for client and the other for ngrok. 34 | 35 | 1. In terminal 1, run: `npm run dev`. This runs the development server(webpack-dev-server) at port 8080. 36 | 2. In terminal 2, point ngrok to 8080 by running: `/path/to/ngrok http 8080`. You'll see ngrok w/ urls as shown below. Simpy use the **https** one. 37 | 38 | 3. Open up your Visualforce page and page the code below. Update the bundle.js file's url to `/bundle.js`. 39 | 40 | ``` 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ``` 66 | 67 | 68 | #Production (Visualforce) 69 | 70 | 1. Generate the latest React app by running: `npm run build` 71 | 2. This file will be created in the `public` folder 72 | 3. Upload the file to static resource 73 | 4. Change the URL in your Visualforce to point to the `bundle.js` in Static resource. 74 | 75 | 76 | #Learn More 77 | 1. Visualforce Remote Objects 78 | 2. jsforce (nodejs lib) 79 | 80 | 81 | #React Redux Blogs 82 | Please check out the following blogs to learn more: 83 | 84 | 1. A Guide For Building A React Redux CRUD App 85 | 2. Adding A Robust Form Validation To React Redux Apps 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | 8 | var posts = require('./routes/posts'); 9 | 10 | var app = express(); 11 | 12 | app.engine('html', require('ejs').renderFile); 13 | app.set('view engine', 'html'); 14 | 15 | // uncomment after placing your favicon in /public 16 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 17 | app.use(function(req, res, next) { 18 | res.header("Access-Control-Allow-Origin", "*"); 19 | res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE'); 20 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 21 | next(); 22 | }); 23 | 24 | var staticPath = 'public'; 25 | console.log("staticPath = " + staticPath); 26 | 27 | app.use(logger('dev')); 28 | app.use(bodyParser.json()); 29 | app.use(bodyParser.urlencoded({ extended: false })); 30 | app.use(cookieParser()); 31 | 32 | app.use('/api', posts); 33 | app.use(express.static(staticPath)); 34 | app.use('/', express.static(staticPath)); 35 | app.use('/posts/*', express.static(staticPath)); 36 | app.use('/new/*', express.static(staticPath)); 37 | 38 | // catch 404 and forward to error handler 39 | app.use(function(req, res, next) { 40 | var err = new Error('Not Found'); 41 | err.status = 404; 42 | next(err); 43 | }); 44 | 45 | 46 | // error handlers 47 | 48 | // development error handler 49 | // will print stacktrace 50 | if (app.get('env') === 'development') { 51 | app.use(function(err, req, res, next) { 52 | res.status(err.status || 500); 53 | res.render('error', { 54 | message: err.message, 55 | error: err 56 | }); 57 | }); 58 | } 59 | 60 | // production error handler 61 | // no stacktraces leaked to user 62 | app.use(function(err, req, res, next) { 63 | res.status(err.status || 500); 64 | res.render('error', { 65 | message: err.message, 66 | error: {} 67 | }); 68 | }); 69 | 70 | 71 | module.exports = app; 72 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Redux Blog CRUD app", 3 | "description": "A sample Blog app that shows how to build React Redux CRUD app", 4 | "repository": "https://github.com/rajaraodv/react-redux-blog", 5 | "logo": "http://node-js-sample.herokuapp.com/node.svg", 6 | "keywords": ["node", "express", "React", "React.js", "React", "Redux", "JavaScript", "MongoDB"], 7 | "image": "heroku/nodejs", 8 | "env": { 9 | "NODE_ENV": { 10 | "description": "Node environment", 11 | "value": "production" 12 | } 13 | }, 14 | "addons": ["mongolab"] 15 | } -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('blog-server:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /ngrok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajaraodv/react-redux-blog-vf/0fd67bd10f629d401483ecfe9bf32789ca3e5781/ngrok.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www", 7 | "build": "node node_modules/.bin/webpack", 8 | "dev": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js" 9 | }, 10 | "engines": { 11 | "node": "5.2.0", 12 | "npm": "3.3.12" 13 | }, 14 | "devDependencies": { 15 | "babel-core": "^6.2.1", 16 | "babel-loader": "^6.2.0", 17 | "babel-preset-es2015": "^6.1.18", 18 | "babel-preset-react": "^6.1.18", 19 | "webpack": "^1.12.9", 20 | "webpack-dev-server": "^1.14.0" 21 | }, 22 | "dependencies": { 23 | "axios": "^0.9.0", 24 | "babel-preset-stage-1": "^6.1.18", 25 | "body-parser": "~1.13.2", 26 | "cookie-parser": "~1.3.5", 27 | "debug": "~2.2.0", 28 | "ejs": "^2.4.1", 29 | "express": "~4.13.1", 30 | "jade": "~1.11.0", 31 | "jsforce": "^1.6.0", 32 | "lodash": "^3.10.1", 33 | "mongoose": "^4.4.6", 34 | "mongoose-timestamp": "^0.5.0", 35 | "morgan": "~1.6.1", 36 | "node-salesforce": "^0.8.0", 37 | "react": "^0.14.3", 38 | "react-dom": "^0.14.3", 39 | "react-redux": "^4.0.0", 40 | "react-router": "^2.0.0-rc5", 41 | "redux": "^3.0.4", 42 | "redux-form": "^4.1.3", 43 | "redux-promise": "^0.5.1", 44 | "serve-favicon": "~2.3.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React-Redux-CRUD-App", 3 | "version": "1.0.0", 4 | "description": "Shows how to build React Redux CRUD apps", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js" 8 | }, 9 | "author": "@rajaraodv", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "babel-core": "^6.2.1", 13 | "babel-loader": "^6.2.0", 14 | "babel-preset-es2015": "^6.1.18", 15 | "babel-preset-react": "^6.1.18", 16 | "webpack": "^1.12.9", 17 | "webpack-dev-server": "^1.14.0" 18 | }, 19 | "dependencies": { 20 | "axios": "^0.9.0", 21 | "babel-preset-stage-1": "^6.1.18", 22 | "lodash": "^3.10.1", 23 | "react": "^0.14.3", 24 | "react-dom": "^0.14.3", 25 | "react-redux": "^4.0.0", 26 | "react-router": "^2.0.0-rc5", 27 | "redux": "^3.0.4", 28 | "redux-form": "^4.1.3", 29 | "redux-promise": "^0.5.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {getPosts, getPost, createNewPost, validateFields, deleteSinglePost} from '../api/index'; 3 | 4 | //Post list 5 | export const FETCH_POSTS = 'FETCH_POSTS'; 6 | export const FETCH_POSTS_SUCCESS = 'FETCH_POSTS_SUCCESS'; 7 | export const FETCH_POSTS_FAILURE = 'FETCH_POSTS_FAILURE'; 8 | export const RESET_POSTS = 'RESET_POSTS'; 9 | 10 | //Create new post 11 | export const CREATE_POST = 'CREATE_POST'; 12 | export const CREATE_POST_SUCCESS = 'CREATE_POST_SUCCESS'; 13 | export const CREATE_POST_FAILURE = 'CREATE_POST_FAILURE'; 14 | export const RESET_NEW_POST = 'RESET_NEW_POST'; 15 | 16 | //Validate post fields like Title, Categries on the server 17 | export const VALIDATE_POST_FIELDS = 'VALIDATE_POST_FIELDS'; 18 | export const VALIDATE_POST_FIELDS_SUCCESS = 'VALIDATE_POST_FIELDS_SUCCESS'; 19 | export const VALIDATE_POST_FIELDS_FAILURE = 'VALIDATE_POST_FIELDS_FAILURE'; 20 | export const RESET_POST_FIELDS = 'RESET_POST_FIELDS'; 21 | 22 | //Fetch post 23 | export const FETCH_POST = 'FETCH_POST'; 24 | export const FETCH_POST_SUCCESS = 'FETCH_POST_SUCCESS'; 25 | export const FETCH_POST_FAILURE = 'FETCH_POST_FAILURE'; 26 | export const RESET_ACTIVE_POST = 'RESET_ACTIVE_POST'; 27 | 28 | //Delete post 29 | export const DELETE_POST = 'DELETE_POST'; 30 | export const DELETE_POST_SUCCESS = 'DELETE_POST_SUCCESS'; 31 | export const DELETE_POST_FAILURE = 'DELETE_POST_FAILURE'; 32 | export const RESET_DELETED_POST = 'RESET_DELETED_POST'; 33 | 34 | 35 | const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:3000/api' : '/api'; 36 | export function fetchPosts() { 37 | return { 38 | type: FETCH_POSTS, 39 | payload: getPosts() 40 | }; 41 | } 42 | 43 | export function fetchPostsSuccess(posts) { 44 | return { 45 | type: FETCH_POSTS_SUCCESS, 46 | payload: posts 47 | }; 48 | } 49 | 50 | export function fetchPostsFailure(error) { 51 | return { 52 | type: FETCH_POSTS_FAILURE, 53 | payload: error 54 | }; 55 | } 56 | 57 | export function validatePostFields(props) { 58 | return { 59 | type: VALIDATE_POST_FIELDS, 60 | payload: validateFields(props) 61 | }; 62 | } 63 | 64 | export function validatePostFieldsSuccess() { 65 | return { 66 | type: VALIDATE_POST_FIELDS_SUCCESS 67 | }; 68 | } 69 | 70 | export function validatePostFieldsFailure(error) { 71 | return { 72 | type: VALIDATE_POST_FIELDS_FAILURE, 73 | payload: error 74 | }; 75 | } 76 | 77 | export function resetPostFields() { 78 | return { 79 | type: RESET_POST_FIELDS 80 | } 81 | }; 82 | 83 | 84 | export function createPost(props) { 85 | 86 | return { 87 | type: CREATE_POST, 88 | payload: createNewPost(props) 89 | }; 90 | } 91 | 92 | export function createPostSuccess(newPost) { 93 | return { 94 | type: CREATE_POST_SUCCESS, 95 | payload: newPost 96 | }; 97 | } 98 | 99 | export function createPostFailure(error) { 100 | return { 101 | type: CREATE_POST_FAILURE, 102 | payload: error 103 | }; 104 | } 105 | 106 | export function resetNewPost() { 107 | return { 108 | type: RESET_NEW_POST 109 | } 110 | }; 111 | 112 | export function resetDeletedPost() { 113 | return { 114 | type: RESET_DELETED_POST 115 | } 116 | }; 117 | 118 | export function fetchPost(id) { 119 | return { 120 | type: FETCH_POST, 121 | payload: getPost(id) 122 | }; 123 | } 124 | 125 | 126 | export function fetchPostSuccess(activePost) { 127 | return { 128 | type: FETCH_POST_SUCCESS, 129 | payload: activePost 130 | }; 131 | } 132 | 133 | export function fetchPostFailure(error) { 134 | return { 135 | type: FETCH_POST_FAILURE, 136 | payload: error 137 | }; 138 | } 139 | 140 | export function resetActivePost() { 141 | return { 142 | type: RESET_ACTIVE_POST 143 | } 144 | }; 145 | 146 | export function deletePost(id) { 147 | 148 | return { 149 | type: DELETE_POST, 150 | payload: deleteSinglePost(id) 151 | }; 152 | } 153 | 154 | export function deletePostSuccess(deletedPost) { 155 | return { 156 | type: DELETE_POST_SUCCESS, 157 | payload: deletedPost 158 | }; 159 | } 160 | 161 | export function deletePostFailure(response) { 162 | return { 163 | type: DELETE_POST_FAILURE, 164 | payload: response 165 | }; 166 | } -------------------------------------------------------------------------------- /public/src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const inVF = location.href.indexOf('apex') > 0 ? true : false; 4 | const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:3000/api' : '/api'; 5 | 6 | export function getPosts() { 7 | if (!inVF) { //local - use axios 8 | return axios.get(`${ROOT_URL}/posts?query=SELECT Id, Name,Categories__c FROM Post__c`); 9 | } 10 | 11 | // Wrap RemoteObject's function in a Pomise so we can use Redux-promise like axios lib 12 | return new Promise((resolve, reject) => { 13 | 14 | var post = new SObjectModel.Post(); 15 | 16 | // Use the Remote Object to query for 10 items records 17 | post.retrieve({limit: 10 }, function(err, records, event) { 18 | if (err) { 19 | alert(err.message); 20 | return reject({data: {message: err.message}, status:400}); 21 | } 22 | //convert result object to Array to match axios+jsforce(local setup) 23 | var records = event.result.records; 24 | var posts = Object.keys(records).map(function (key) {return records[key]}); 25 | 26 | return resolve({data: {records: posts}}); //note: wrapping in 'data' to match axios + jsforce local setup 27 | }); 28 | }); 29 | } 30 | 31 | export function getPost(id) { 32 | if (!inVF) { //local - use axios 33 | return axios.get(`${ROOT_URL}/posts/${id}`); 34 | } 35 | 36 | // Wrap RemoteObject's function in a Pomise so we can use Redux-promise like axios lib 37 | return new Promise((resolve, reject) => { 38 | 39 | var post = new SObjectModel.Post(); 40 | 41 | //Retreive post by id 42 | post.retrieve({where: {Id: {eq: id }}}, function(err, records, event) { 43 | if (err) { 44 | alert(err.message); 45 | return reject({data: {message: err.message}, status:400}); 46 | } 47 | //convert result object to Array to match axios+jsforce(local setup) 48 | var record = event.result.records["0"] || {}; 49 | 50 | return resolve({data:record}); //note: wrapping in 'data' to match axios + jsforce local setup 51 | }); 52 | }); 53 | } 54 | 55 | export function validateFields(props) { 56 | if (!inVF) { //local - use axios 57 | return axios.post(`${ROOT_URL}/validatePostFields`, props); 58 | } 59 | 60 | // Wrap RemoteObject's function in a Pomise so we can use Redux-promise like axios lib 61 | return new Promise((resolve, reject) => { 62 | var post = new SObjectModel.Post(); 63 | //Retreive post by title 64 | post.retrieve({where: {Name: {eq: props.Name }}}, function(err, records, event) { 65 | if (err) { 66 | alert(err.message); 67 | return reject({data: {message: err.message}, status:400}); 68 | } 69 | //convert result object to Array to match axios+jsforce(local setup) 70 | var record = event.result.records["0"] ? {'Name': 'Name already exists', status: 400} : {}; 71 | 72 | return resolve({data:record, status: 200}); //note: wrapping in 'data' to match axios + jsforce local setup 73 | }); 74 | }); 75 | } 76 | 77 | 78 | export function createNewPost(props) { 79 | if (!inVF) { //use axios 80 | return axios.post(`${ROOT_URL}/posts`, props); 81 | } 82 | 83 | // Wrap RemoteObject's function in a Pomise so we can use Redux-promise like axios lib 84 | return new Promise((resolve, reject) => { 85 | var post = new SObjectModel.Post(); 86 | //Retreive post by id 87 | post.create(props, function(err, records, event) { 88 | if (err) { 89 | alert(err.message); 90 | return reject({data: {message: err.message}, status:400}); 91 | } 92 | return resolve({data:event.result, status: 200}); 93 | }); 94 | }); 95 | } 96 | 97 | 98 | export function deleteSinglePost(id) { 99 | if (!inVF) { //local - use axios 100 | return axios.delete(`${ROOT_URL}/posts/${id}`); 101 | } 102 | 103 | // Wrap RemoteObject's function in a Pomise so we can use Redux-promise like axios lib 104 | return new Promise((resolve, reject) => { 105 | var post = new SObjectModel.Post(); 106 | //Retreive post by id 107 | post.del([id], function(err, records, event) { 108 | if (err) { 109 | alert(err.message); 110 | return reject({data: {message: err.message}, status:400}); 111 | } 112 | return resolve({data:event.result, status: 200}); //note: wrapping in 'data' to match axios + jsforce local setup 113 | }); 114 | }); 115 | } 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /public/src/components/PostDetails.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | //import { connect } from 'react-redux'; 3 | //import { fetchPost, deletePost } from '../actions/index'; 4 | import { Link } from 'react-router'; 5 | 6 | class PostDetails extends Component { 7 | static contextTypes = { 8 | router: PropTypes.object 9 | }; 10 | 11 | componentWillMount() { 12 | //Important! If your component is navigating based on some global state(from say componentWillReceiveProps) 13 | //always reset that global state back to null when you REMOUNT 14 | this.props.resetMe(); 15 | } 16 | 17 | componentDidMount() { 18 | this.props.fetchPost(this.props.postId); 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | if(nextProps.activePost.error) { 23 | alert('No such post'); 24 | this.context.router.push('/'); 25 | } 26 | } 27 | 28 | render() { 29 | const { post } = this.props.activePost; 30 | 31 | if (!post) { 32 | return Loading...; 33 | } 34 | 35 | return ( 36 | 37 | {post.Name} 38 | Categories: {post.Categories__c} 39 | {post.Content__c} 40 | 41 | ); 42 | } 43 | } 44 | 45 | export default PostDetails; 46 | -------------------------------------------------------------------------------- /public/src/components/PostsForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | import Header from '../containers/HeaderContainer.js'; 4 | 5 | class PostsForm extends Component { 6 | static contextTypes = { 7 | router: PropTypes.object 8 | }; 9 | 10 | componentWillMount() { 11 | //Important! If your component is navigating based on some global state(from say componentWillReceiveProps) 12 | //always reset that global state back to null when you REMOUNT 13 | this.props.resetMe(); 14 | } 15 | 16 | componentWillReceiveProps(nextProps) { 17 | if(nextProps.newPost.post && !nextProps.newPost.error) { 18 | this.context.router.push('/'); 19 | } 20 | } 21 | 22 | render() { 23 | const {asyncValidating, fields: { Name, Categories__c, Content__c }, handleSubmit, submitting } = this.props; 24 | 25 | return ( 26 | 27 | 28 | 29 | Name* 30 | 31 | 32 | {Name.touched ? Name.error : ''} 33 | 34 | 35 | {asyncValidating === 'Name'? 'validating..': ''} 36 | 37 | 38 | 39 | 40 | Categories* 41 | 42 | 43 | {Categories__c.touched ? Categories__c.error : ''} 44 | 45 | 46 | 47 | 48 | Content* 49 | 50 | 51 | {Content__c.touched ? Content__c.error : ''} 52 | 53 | 54 | 55 | Submit 56 | Cancel 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | This form has "Form Validations"! 66 | 67 | Learn how to implement it by going through: Adding A Robust Form Validation To React Redux Apps 68 | 69 | 70 | 71 | 72 | 73 | 74 | ); 75 | } 76 | } 77 | 78 | export default PostsForm; -------------------------------------------------------------------------------- /public/src/components/PostsList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | class PostsList extends Component { 5 | componentWillMount() { 6 | this.props.fetchPosts(); 7 | } 8 | 9 | renderCategories(categories) { 10 | return categories.map((c) => { 11 | c = c.trim(); 12 | return ( 13 | {" " + c + " "} 14 | ); 15 | }); 16 | } 17 | 18 | renderPosts(posts) { 19 | return posts.map((post) => { 20 | return ( 21 | 22 | 23 | {post.Name} 24 | 25 | {this.renderCategories(post.Categories__c ? post.Categories__c.split(',') : [])} 26 | 27 | ); 28 | }); 29 | } 30 | 31 | render() { 32 | if(this.props.loading) { 33 | return PostsLoading... 34 | } else if(this.props.error) { 35 | return PostsThere is an error 36 | } else if(!this.props.posts) { 37 | return PostsThere are no posts 38 | } 39 | 40 | return ( 41 | 42 | 43 | {this.renderPosts(this.props.posts)} 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | 51 | export default PostsList; 52 | -------------------------------------------------------------------------------- /public/src/components/header.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | 5 | class Header extends Component { 6 | static contextTypes = { 7 | router: PropTypes.object 8 | }; 9 | 10 | componentWillMount() { 11 | //Important! If your component is navigating based on some global state(from say componentWillReceiveProps) 12 | //always reset that global state back to null when you REMOUNT 13 | this.props.resetMe(); 14 | } 15 | 16 | componentWillReceiveProps(nextProps) { 17 | if(nextProps.deletedPost.error) { 18 | alert('Could not delete. Please try again.'); 19 | } else if(nextProps.deletedPost.post && !nextProps.deletedPost.error) { 20 | this.context.router.push('/'); 21 | } 22 | } 23 | 24 | renderLinks() { 25 | const { type } = this.props; 26 | if(type === 'posts_index') { 27 | return ( 28 | 29 | 30 | 31 | New Post 32 | 33 | 34 | 35 | ); 36 | } else if(type === 'posts_new') { 37 | return ( 38 | 39 | 40 | Back To Index 41 | 42 | 43 | ); 44 | } else if(type === 'posts_show') { 45 | return ( 46 | 47 | 48 | Back To Index 49 | 50 | 51 | {this.props.onDeleteClick()}}>Delete Post 52 | 53 | 54 | ); 55 | } 56 | }; 57 | 58 | render() { 59 | return ( 60 | 61 | 62 | {this.renderLinks()} 63 | 64 | 65 | ); 66 | } 67 | } 68 | 69 | export default Header -------------------------------------------------------------------------------- /public/src/containers/HeaderContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchPosts, resetDeletedPost, deletePost, deletePostSuccess, deletePostFailure } from '../actions/index'; 4 | import Header from '../components/header.js'; 5 | 6 | 7 | 8 | function mapStateToProps(state) { 9 | return { 10 | deletedPost: state.posts.deletedPost 11 | }; 12 | } 13 | 14 | const mapDispatchToProps = (dispatch, ownProps) => { 15 | return { 16 | onDeleteClick: () => { 17 | dispatch(deletePost(ownProps.postId)) 18 | .then((response) => { 19 | !response.error ? dispatch(deletePostSuccess(response.payload.data)) : dispatch(deletePostFailure(response.payload.data)); 20 | }); 21 | }, 22 | resetMe: () =>{ 23 | dispatch(resetDeletedPost()); 24 | } 25 | } 26 | } 27 | 28 | 29 | export default connect(mapStateToProps, mapDispatchToProps)(Header); 30 | -------------------------------------------------------------------------------- /public/src/containers/PostDetailsContainer.js: -------------------------------------------------------------------------------- 1 | import PostDetails from '../components/PostDetails.js'; 2 | import { fetchPost, fetchPostSuccess, fetchPostFailure, resetActivePost } from '../actions/index'; 3 | import { connect } from 'react-redux'; 4 | 5 | 6 | 7 | function mapStateToProps(globalState, ownProps) { 8 | return { activePost: globalState.posts.activePost, postId: ownProps.id }; 9 | } 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | return { 13 | fetchPost: (id) => { 14 | dispatch(fetchPost(id)) 15 | .then((data) => 16 | { 17 | !data.error ? dispatch(fetchPostSuccess(data.payload)) : dispatch(fetchPostFailure(data.payload)); 18 | }) 19 | }, 20 | resetMe: () =>{ 21 | dispatch(resetActivePost()); 22 | } 23 | } 24 | } 25 | 26 | 27 | export default connect(mapStateToProps, mapDispatchToProps)(PostDetails); 28 | -------------------------------------------------------------------------------- /public/src/containers/PostFormContainer.js: -------------------------------------------------------------------------------- 1 | import PostsForm from '../components/PostsForm.js'; 2 | import { createPost, createPostSuccess, createPostFailure, resetNewPost, validatePostFields, validatePostFieldsSuccess, validatePostFieldsFailure } from '../actions/index'; 3 | import { reduxForm } from 'redux-form'; 4 | 5 | //Client side validation 6 | function validate(values) { 7 | const errors = {}; 8 | 9 | if (!values.Name || values.Name.trim() === '') { 10 | errors.Name = 'Enter a Name'; 11 | } 12 | if (!values.Categories__c || values.Categories__c.trim() === '') { 13 | errors.Categories__c = 'Enter Categories'; 14 | } 15 | if(!values.Content__c || values.Content__c.trim() === '') { 16 | errors.Content__c = 'Enter some Content'; 17 | } 18 | 19 | return errors; 20 | } 21 | 22 | //For instant async server validation 23 | const asyncValidate = (values, dispatch) => { 24 | 25 | return new Promise((resolve, reject) => { 26 | 27 | dispatch(validatePostFields(values)) 28 | .then((response) => { 29 | let data = response.payload.data; 30 | //if status is not 200 or any one of the fields exist, then there is a field error 31 | if(response.payload.status != 200 || data.Name || data.Categories__c || data.Content__c) { 32 | //let other components know of error by updating the redux` state 33 | dispatch(validatePostFieldsFailure(response.payload)); 34 | reject(data); //this is for redux-form itself 35 | } else { 36 | //let other components know that everything is fine by updating the redux` state 37 | dispatch(validatePostFieldsSuccess(response.payload)); //ps: this is same as dispatching RESET_POST_FIELDS 38 | resolve();//this is for redux-form itself 39 | } 40 | }); 41 | }); 42 | }; 43 | 44 | //For any field errors upon submission (i.e. not instant check) 45 | const validateAndCreatePost = (values, dispatch) => { 46 | 47 | return new Promise((resolve, reject) => { 48 | dispatch(createPost(values)) 49 | .then((response) => { 50 | let data = response.payload.data; 51 | //if any one of these exist, then there is a field error 52 | if(response.payload.status != 200) { 53 | //let other components know of error by updating the redux` state 54 | dispatch(createPostFailure(response.payload)); 55 | reject(data); //this is for redux-form itself 56 | } else { 57 | //let other components know that everything is fine by updating the redux` state 58 | dispatch(createPostSuccess(response.payload)); 59 | resolve();//this is for redux-form itself 60 | } 61 | }); 62 | }); 63 | }; 64 | 65 | 66 | 67 | const mapDispatchToProps = (dispatch) => { 68 | return { 69 | createPost: validateAndCreatePost, 70 | resetMe: () =>{ 71 | dispatch(resetNewPost()); 72 | } 73 | } 74 | } 75 | 76 | 77 | function mapStateToProps(state, ownProps) { 78 | return { 79 | newPost: state.posts.newPost 80 | }; 81 | } 82 | 83 | 84 | // connect: first argument is mapStateToProps, 2nd is mapDispatchToProps 85 | // reduxForm: 1st is form config, 2nd is mapStateToProps, 3rd is mapDispatchToProps 86 | export default reduxForm({ 87 | form: 'PostsNewForm', 88 | fields: ['Name', 'Categories__c', 'Content__c'], 89 | asyncValidate, 90 | asyncBlurFields: ['Name'], 91 | validate 92 | }, mapStateToProps, mapDispatchToProps)(PostsForm); 93 | -------------------------------------------------------------------------------- /public/src/containers/PostsListContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { fetchPosts, fetchPostsSuccess, fetchPostsFailure } from '../actions/index'; 3 | 4 | import PostsList from '../components/PostsList'; 5 | 6 | 7 | const mapStateToProps = (state) => { 8 | return { 9 | posts: state.posts.postsList.posts, 10 | loading: state.posts.postsList.loading, 11 | error: state.posts.postsList.error 12 | }; 13 | } 14 | 15 | const mapDispatchToProps = (dispatch) => { 16 | return { 17 | fetchPosts: () => { 18 | dispatch(fetchPosts()).then((response) => { 19 | let data = response.payload.data ? response.payload.data : {data: 'Network Error'}; 20 | !response.error ? dispatch(fetchPostsSuccess(data)) : dispatch(fetchPostsFailure(data)); 21 | }); 22 | } 23 | } 24 | } 25 | 26 | 27 | const PostsListContainer = connect(mapStateToProps, mapDispatchToProps)(PostsList) 28 | 29 | export default PostsListContainer 30 | -------------------------------------------------------------------------------- /public/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { createStore, applyMiddleware } from 'redux'; 5 | import { Router, hashHistory } from 'react-router'; 6 | import reducers from './reducers'; 7 | import routes from './routes'; 8 | import promise from 'redux-promise'; 9 | 10 | const createStoreWithMiddleware = applyMiddleware( 11 | promise 12 | )(createStore); 13 | 14 | ReactDOM.render( 15 | 16 | 17 | 18 | , document.getElementById('body')); 19 | -------------------------------------------------------------------------------- /public/src/pages/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | export default class App extends Component { 5 | render() { 6 | return ( 7 | 8 | {this.props.children} 9 | 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /public/src/pages/PostsIndex.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import HeaderContainer from '../containers/HeaderContainer.js'; 3 | import PostsList from '../containers/PostsListContainer.js'; 4 | 5 | class PostsIndex extends Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | } 15 | 16 | 17 | export default PostsIndex; 18 | -------------------------------------------------------------------------------- /public/src/pages/PostsNew.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import HeaderContainer from '../containers/HeaderContainer.js'; 3 | import PostFormContainer from '../containers/PostFormContainer.js'; 4 | 5 | class PostsNew extends Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | } 15 | 16 | 17 | export default PostsNew; 18 | -------------------------------------------------------------------------------- /public/src/pages/PostsShow.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchPost, deletePost } from '../actions/index'; 4 | import { Link } from 'react-router'; 5 | import Header from '../containers/HeaderContainer.js'; 6 | import PostDetailsContainer from '../containers/PostDetailsContainer.js'; 7 | 8 | class PostsShow extends Component { 9 | static contextTypes = { 10 | router: PropTypes.object 11 | }; 12 | 13 | onDeleteClick() { 14 | this.props.deletePost(this.props.params.id) 15 | .then(() => { this.context.router.push('/'); }); 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | } 27 | 28 | export default PostsShow; 29 | -------------------------------------------------------------------------------- /public/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import PostsReducer from './reducer_posts'; 3 | import { reducer as formReducer } from 'redux-form'; 4 | 5 | const rootReducer = combineReducers({ 6 | posts: PostsReducer, //<-- Posts 7 | form: formReducer // <-- redux-form 8 | }); 9 | 10 | export default rootReducer; 11 | -------------------------------------------------------------------------------- /public/src/reducers/reducer_posts.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_POSTS, FETCH_POSTS_SUCCESS, FETCH_POSTS_FAILURE, RESET_POSTS, 3 | FETCH_POST, FETCH_POST_SUCCESS, FETCH_POST_FAILURE, RESET_ACTIVE_POST, 4 | CREATE_POST, CREATE_POST_SUCCESS, CREATE_POST_FAILURE, RESET_NEW_POST, 5 | DELETE_POST, DELETE_POST_SUCCESS, DELETE_POST_FAILURE, RESET_DELETED_POST, 6 | VALIDATE_POST_FIELDS,VALIDATE_POST_FIELDS_SUCCESS, VALIDATE_POST_FIELDS_FAILURE, RESET_POST_FIELDS 7 | } from '../actions/index'; 8 | 9 | 10 | const INITIAL_STATE = { postsList: {posts: [], error:null, loading: false}, 11 | newPost:{post:null, error: null, loading: false}, 12 | activePost:{post:null, error:null, loading: false}, 13 | deletedPost: {post: null, error:null, loading: false}, 14 | }; 15 | 16 | export default function(state = INITIAL_STATE, action) { 17 | switch(action.type) { 18 | 19 | case FETCH_POSTS:// start fetching posts and set loading = true 20 | return { ...state, postsList: {posts:[], error: null, loading: true} }; 21 | case FETCH_POSTS_SUCCESS:// return list of posts and make loading = false 22 | return { ...state, postsList: {posts: action.payload.records, error:null, loading: false} }; 23 | case FETCH_POSTS_FAILURE:// return error and make loading = false 24 | return { ...state, postsList: {posts: null, error: action.payload, loading: false} }; 25 | case RESET_POSTS:// reset postList to initial state 26 | return { ...state, postsList: {posts: null, error:null, loading: false} }; 27 | 28 | case FETCH_POST: 29 | return { ...state, activePost:{...state.activePost, loading: true}}; 30 | case FETCH_POST_SUCCESS: 31 | return { ...state, activePost: {post: action.payload.data, error:null, loading: false}}; 32 | case FETCH_POST_FAILURE: 33 | return { ...state, activePost: {post: null, error:action.payload.data, loading:false}}; 34 | case RESET_ACTIVE_POST: 35 | return { ...state, activePost: {post: null, error:null, loading: false}}; 36 | 37 | case CREATE_POST: 38 | return {...state, newPost: {...state.newPost, loading: true}} 39 | case CREATE_POST_SUCCESS: 40 | return {...state, newPost: {post:action.payload, error:null, loading: false}} 41 | case CREATE_POST_FAILURE: 42 | return {...state, newPost: {post:null, error:action.payload.data, loading: false}} 43 | case RESET_NEW_POST: 44 | return {...state, newPost:{post:null, error:null, loading: false}} 45 | 46 | 47 | case DELETE_POST: 48 | return {...state, deletedPost: {...state.deletedPost, loading: true}} 49 | case DELETE_POST_SUCCESS: 50 | return {...state, deletedPost: {post:action.payload, error:null, loading: false}} 51 | case DELETE_POST_FAILURE: 52 | return {...state, deletedPost: {post:null, error:action.payload, loading: false}} 53 | case RESET_DELETED_POST: 54 | return {...state, deletedPost:{post:null, error:null, loading: false}} 55 | 56 | case VALIDATE_POST_FIELDS: 57 | return {...state, newPost:{...state.newPost, error: null, loading: true}} 58 | case VALIDATE_POST_FIELDS_SUCCESS: 59 | return {...state, newPost:{...state.newPost, error: null, loading: false}} 60 | case VALIDATE_POST_FIELDS_FAILURE: 61 | let result = action.payload.data; 62 | let error = {title: result.title, categories: result.categories, description: result.description}; 63 | return {...state, newPost:{...state.newPost, error: error, loading: false}} 64 | case RESET_POST_FIELDS: 65 | return {...state, newPost:{...state.newPost, error: null, loading: null}} 66 | default: 67 | return state; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | 4 | import App from './pages/App'; 5 | import PostsIndex from './pages/PostsIndex'; 6 | import PostsNew from './pages/PostsNew'; 7 | import PostsShow from './pages/PostsShow'; 8 | 9 | // let prefix = location.pathname; 10 | // let index = prefix; 11 | // let postsNew = prefix + '/posts/new'; 12 | // let postsShow = prefix + '/posts/:id'; 13 | 14 | let index = '/'; 15 | let postsNew = '/posts/new'; 16 | let postsShow = '/posts/:id'; 17 | 18 | export default ( 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /public/style/style.css: -------------------------------------------------------------------------------- 1 | form a { 2 | margin-left: 5px; 3 | } 4 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: 'Express' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /routes/posts.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var jsforce = require('jsforce'); 4 | var conn = new jsforce.Connection({ 5 | serverUrl: 'https://ltng1-dev-ed.my.salesforce.com', 6 | sessionId: '00Di0000000JEm3!AQ4AQEoB90cYo66ZD8DLTaegtPMrnt3LqoUHaKnRh1uP.mU8cAlJudaqx_jJhASRVx4pcPgCLwWOjRTu2fRrLIR6DgBYciDJ' 7 | }); 8 | 9 | 10 | 11 | router.get('/posts', function(req, res, next) { 12 | var q = req.query.query; 13 | conn.query(q, function(err, result) { 14 | if (err) { 15 | return res.status(400).json(err); 16 | } 17 | res.json(result); 18 | }); 19 | }); 20 | 21 | router.post('/posts', function(req, res, next) { 22 | var body = req.body; 23 | var post = { 24 | 'Name': body.Name, 25 | 'Categories__c': body.Categories__c, 26 | 'Content__c': body.Content__c 27 | }; 28 | 29 | conn.sobject("Post__c").create(post, function(err, result) { 30 | if (err) { 31 | return res.status(400).json(err); 32 | } 33 | res.json(result); 34 | }); 35 | }); 36 | 37 | router.get('/posts/:id', function(req, res, next) { 38 | var id = req.params.id; 39 | conn.sobject("Post__c").retrieve(id, function(err, result) { 40 | if (err) { 41 | return res.status(400).json(err); 42 | } 43 | res.json(result); 44 | }); 45 | }); 46 | 47 | router.delete('/posts/:id', function(req, res, next) { 48 | var id = req.params.id; 49 | conn.sobject("Post__c").destroy(id, function(err, result) { 50 | if (err) { 51 | return res.status(400).json(err); 52 | } 53 | res.json(result); 54 | }); 55 | }); 56 | 57 | 58 | router.post('/validatePostFields', function(req, res, next) { 59 | var body = req.body; 60 | var Name = body.Name ? body.Name.trim() : ''; 61 | if(!Name || Name === '' || Name.indexOf(' ') > 0) { 62 | return res.status(400).json({message: 'bad request'}); 63 | } 64 | 65 | conn.query('SELECT Id FROM Post__c WHERE NAME = "' + Name + '"', function(err, result) { 66 | if (err) { 67 | return res.status(400).json(err); 68 | } 69 | console.dir(result); 70 | res.json(result); 71 | }); 72 | 73 | // Post.findOne({'title': new RegExp(title, "i") }, function(err, post){ 74 | // if (err) { 75 | // console.log(err); 76 | // return res.json({error: 'Could not find post for title uniqueness'}); 77 | // } 78 | // if(post) { 79 | // res.json({title: 'Title "'+title+'" is not unique!'}); 80 | // } else { 81 | // return res.json({}); 82 | // } 83 | 84 | // }); 85 | }); 86 | 87 | 88 | module.exports = router; -------------------------------------------------------------------------------- /sample-vf-page.vfp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /views/error.html: -------------------------------------------------------------------------------- 1 | Not Found -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: [ 3 | './public/src/index.js' 4 | ], 5 | output: { 6 | path: __dirname + '/public/', 7 | publicPath: '/', 8 | filename: 'bundle.js' 9 | }, 10 | module: { 11 | loaders: [{ 12 | exclude: /node_modules/, 13 | loader: 'babel' 14 | }] 15 | }, 16 | resolve: { 17 | extensions: ['', '.js', '.jsx'] 18 | }, 19 | devServer: { 20 | historyApiFallback: true, 21 | contentBase: './public' 22 | } 23 | }; 24 | --------------------------------------------------------------------------------
{post.Content__c}