├── .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 | ` 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 | 30 | 31 |
32 | {Name.touched ? Name.error : ''} 33 |
34 |
35 | {asyncValidating === 'Name'? 'validating..': ''} 36 |
37 |
38 | 39 |
40 | 41 | 42 |
43 | {Categories__c.touched ? Categories__c.error : ''} 44 |
45 |
46 | 47 |
48 | 49 |