├── .gitignore ├── 00 Boilerplate ├── README.md ├── assets │ ├── css │ │ └── styles.css │ └── js │ │ ├── app │ │ ├── App.js │ │ ├── helpers │ │ │ └── domUtils.js │ │ ├── modules │ │ │ └── contactsModule.js │ │ └── services │ │ │ └── contactsService.js │ │ └── index.js ├── index.html └── package.json ├── 01.A Basic integration ├── README.md ├── assets │ ├── css │ │ └── styles.css │ └── js │ │ ├── app │ │ ├── App.js │ │ ├── components │ │ │ ├── ContactPropTypes.js │ │ │ ├── ContactRowComponent.js │ │ │ └── ContactsTableComponent.js │ │ ├── modules │ │ │ └── contactsModule.js │ │ └── services │ │ │ └── contactsService.js │ │ └── index.js ├── index.html └── package.json ├── 01.B Move To JSX ├── README.md ├── gulpfile.js ├── index.html ├── package.json └── src │ ├── css │ └── styles.css │ └── js │ ├── app │ ├── App.js │ ├── components │ │ ├── ContactPropTypes.js │ │ ├── ContactRowComponent.jsx │ │ └── ContactsTableComponent.jsx │ ├── modules │ │ └── contactsModule.jsx │ └── services │ │ └── contactsService.js │ └── index.js ├── 02 Props and Render ├── README.md ├── gulpfile.js ├── index.html ├── package.json └── src │ ├── css │ └── styles.css │ └── js │ ├── app │ ├── App.js │ ├── components │ │ ├── ContactPropTypes.js │ │ ├── ContactRowComponent.jsx │ │ └── ContactsTableComponent.jsx │ ├── modules │ │ └── contactsModule.js │ ├── plugins │ │ └── jquery-react.jsx │ └── services │ │ └── contactsService.js │ └── index.js ├── 03 Stateful Component ├── README.md ├── gulpfile.js ├── index.html ├── package.json └── src │ ├── css │ └── styles.css │ └── js │ ├── app │ ├── App.js │ ├── components │ │ ├── ContactPropTypes.js │ │ ├── ContactRowComponent.jsx │ │ ├── ContactsTableComponent.jsx │ │ └── ContactsTableContainer.jsx │ ├── modules │ │ └── contactsModule.js │ ├── plugins │ │ └── jquery-react.jsx │ └── services │ │ └── contactsService.js │ └── index.js ├── 04 Event Emitters ├── README.md ├── gulpfile.js ├── index.html ├── package.json └── src │ ├── css │ └── styles.css │ └── js │ ├── app │ ├── App.js │ ├── components │ │ ├── ContactPropTypes.js │ │ ├── ContactRowComponent.jsx │ │ ├── ContactsTableComponent.jsx │ │ └── ContactsTableContainer.jsx │ ├── modules │ │ └── contactsModule.jsx │ ├── plugins │ │ └── jquery-pub-sub.js │ └── services │ │ └── contactsService.js │ └── index.js ├── 05.A Angular controllerAS & directive ├── README.md ├── gulpfile.js ├── index.html ├── package.json └── src │ ├── app.module.js │ ├── components │ └── ShowEncoded.jsx │ ├── controllers │ └── main.controller.js │ └── css │ └── styles.css ├── 05.B Angular controllerAS & factory ├── README.md ├── gulpfile.js ├── index.html ├── package.json └── src │ ├── app.module.js │ ├── components │ └── ShowEncoded.jsx │ ├── controllers │ └── main.controller.js │ └── css │ └── styles.css ├── 05.C Angular components & directive ├── README.md ├── gulpfile.js ├── index.html ├── package.json └── src │ ├── app.module.js │ ├── components │ ├── accordion │ │ ├── accordion-panel.jsx │ │ └── accordion.jsx │ └── page-content │ │ ├── page-content.html │ │ └── page-content.js │ └── css │ └── styles.css ├── 99_readme_resources ├── 00boilerplate.gif └── 01boilerplate.gif └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist/ 4 | typings/ 5 | *.orig 6 | .idea/ 7 | */coverage/ 8 | .vscode/ 9 | .editorconfig 10 | yarn.lock 11 | package-lock.json -------------------------------------------------------------------------------- /00 Boilerplate/README.md: -------------------------------------------------------------------------------- 1 | # 00 Boilerplate 2 | 3 | ## Description 4 | 5 | This sample shows a simple and basic implementation of an address book app created using jQuery and ECMAScript5. This little app rendes a simple table of contacts fetched from an API (mocked). 6 | App is structured as follows: 7 | 8 | ``` 9 | . 10 | ├── assets/ 11 | │   ├── css 12 | │   │   └── styles.css 13 | │   └── js/ 14 | │   ├── app/ 15 | │   │   ├── App.js // Global namespace, container of our app 16 | │   │   ├── helpers/ 17 | │   │   │   └── domUtils.js // Helper functions for DOM manipulation 18 | │   │   ├── modules/ 19 | │   │   │   └── contactsModule.js // Contacts module 20 | │   │   └── services/ 21 | │   │   └── contactsService.js // Contacts service for fake server requests 22 | │   └── index.js // Main file to bootstrap our app. 23 | ├── index.html 24 | ├── node_modules/ 25 | └── package.json 26 | ``` 27 | 28 | ## How it works? 29 | 30 | 1. When DOM is loaded our `index.js` file loads the `run` method of our `contactsModule` module. 31 | 32 | 2. This method calls `fetchContacts` that calls our `contactsService` to get an array of contacts. 33 | 34 | 3. When the call is completed then `showContacts` is called to use `domUtils` to create and render the table. 35 | 36 | In next examples we will see how to integrate React and change some jQuery components into React components. 37 | -------------------------------------------------------------------------------- /00 Boilerplate/assets/css/styles.css: -------------------------------------------------------------------------------- 1 | .header { 2 | margin-top: 50px; 3 | } 4 | 5 | .main { 6 | margin-top: 75px; 7 | } 8 | 9 | .form-inline .form-group > *:first-child { 10 | margin: 0 5px 0 20px; 11 | } 12 | 13 | table { 14 | table-layout: fixed; 15 | } 16 | 17 | table > thead > tr > th { 18 | background-color: #555; 19 | color: #FFF; 20 | } 21 | -------------------------------------------------------------------------------- /00 Boilerplate/assets/js/app/App.js: -------------------------------------------------------------------------------- 1 | (function initializeApp(window) { 2 | 'use strict'; 3 | 4 | var App = {}; 5 | window.App = App; 6 | })(window); 7 | -------------------------------------------------------------------------------- /00 Boilerplate/assets/js/app/helpers/domUtils.js: -------------------------------------------------------------------------------- 1 | (function initializeDOMUtils($, App) { 2 | 'use strict'; 3 | 4 | var domUtils = (function () { 5 | var createRow = function (contact) { 6 | var row = $(''); 7 | row.append('' + contact.name + ''); 8 | row.append('' + contact.phone + ''); 9 | row.append('' + contact.email + ''); 10 | return row; 11 | }; 12 | 13 | var renderTable = function (contacts, table) { 14 | var $table = table instanceof $ ? table : $(table); 15 | var html = contacts 16 | .map(createRow) 17 | .reduce(function (text, $row) { 18 | return text + $row.get(0).outerHTML; 19 | }, ''); 20 | 21 | $table.find('tbody').html(html); 22 | }; 23 | 24 | return { 25 | createRow: createRow, 26 | renderTable: renderTable 27 | }; 28 | })(); 29 | 30 | App.domUtils = domUtils; 31 | })(window.jQuery, window.App); 32 | -------------------------------------------------------------------------------- /00 Boilerplate/assets/js/app/modules/contactsModule.js: -------------------------------------------------------------------------------- 1 | (function initializeContactsModule($, App) { 2 | 'use strict'; 3 | 4 | var domUtils = App.domUtils; 5 | var contactsService = App.contactsService; 6 | 7 | var contactsModule = (function () { 8 | var table = '#tableContacts'; 9 | 10 | var run = function () { 11 | fetchContacts(); 12 | }; 13 | 14 | // Simulates server call 15 | var fetchContacts = function () { 16 | $.when(contactsService.fetchContacts()) 17 | .then(function (fetchedContacts) { 18 | showContacts(fetchedContacts); 19 | }); 20 | }; 21 | 22 | // Render table 23 | var showContacts = function (contacts) { 24 | domUtils.renderTable(contacts, table); 25 | }; 26 | 27 | return { 28 | run: run, 29 | }; 30 | })(); 31 | 32 | App.contactsModule = contactsModule; 33 | })(window.jQuery, window.App); 34 | -------------------------------------------------------------------------------- /00 Boilerplate/assets/js/app/services/contactsService.js: -------------------------------------------------------------------------------- 1 | (function initializeContactsService($, App, window) { 2 | 'use strict'; 3 | 4 | var contactsService = (function () { 5 | var REQUEST_DELAY = 600; 6 | var contacts = [{ 7 | name: 'Jose C. Thomson', 8 | phone: 915797511, 9 | email: 'jcthomson@hotmail.com' 10 | }, 11 | { 12 | name: 'Shelia L. Clark', 13 | phone: 956631391, 14 | email: 'sheilac78@gmail.com' 15 | }, 16 | { 17 | name: 'Aaron B. Hudkins', 18 | phone: 660892268, 19 | email: 'flooreb@aol.com' 20 | }]; 21 | 22 | var fetchContacts = function () { 23 | var deferred = $.Deferred(); 24 | window.setTimeout(function () { 25 | deferred.resolve(contacts); 26 | }, REQUEST_DELAY); 27 | return deferred; 28 | }; 29 | 30 | return { 31 | fetchContacts: fetchContacts 32 | }; 33 | })(); 34 | 35 | App.contactsService = contactsService; 36 | })(jQuery, window.App, window) 37 | -------------------------------------------------------------------------------- /00 Boilerplate/assets/js/index.js: -------------------------------------------------------------------------------- 1 | (function initializeMain($, App) { 2 | 'use strict'; 3 | 4 | $(function main() { 5 | App.contactsModule.run(); 6 | }); 7 | })(window.jQuery, window.App); 8 | -------------------------------------------------------------------------------- /00 Boilerplate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boilerplate 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Address Book

13 |
14 |
15 |

My contacts

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
NamePhoneEmail
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /00 Boilerplate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LegacyApps", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Lemoncode/integrate-react-legacy-apps.git", 6 | "author": "Santiago Camargo ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "bootstrap": "^3.3.7", 10 | "jquery": "^3.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /01.A Basic integration/README.md: -------------------------------------------------------------------------------- 1 | # 01 Basic interaction 2 | 3 | In this sample we will introduce our first React component. This component will hold the contacts array in its state and use it to render the table. 4 | 5 | ## Let's get started 6 | 7 | First of all we'll add `react`, `react-dom` and `prop-types` into our app libraries. We manage this with `npm` so we just need to do `npm install` and get this packages. 8 | 9 | ```shell 10 | npm install --save react react-dom prop-types 11 | ``` 12 | 13 | Next we'll link these dependencies into our `index.html`: 14 | 15 | ```html 16 | ... 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ... 26 | ``` 27 | 28 | We'll also remove the `table` element and replace it with a div with id *tableComponent* that will contain our React component. 29 | 30 | Then we'll remove`domUtils.js` helper as we only needed it to render the table. We will replace this with our React table component: 31 | 32 | ```html 33 | ... 34 |
35 |

My contacts

36 |
37 |
38 | ... 39 | ``` 40 | 41 | After that we will create a namespace called `components` inside our `App.js` to hold our React components, and `PropTypes`, that will hold our properties validation for our components: 42 | 43 | ```javascript 44 | (function initializeApp(window) { 45 | 'use strict'; 46 | 47 | var App = {}; 48 | App.components = {}; 49 | App.PropTypes = {}; 50 | 51 | window.App = App; 52 | })(window); 53 | ``` 54 | 55 | Let's write our React `TableComponent`, first we'll create a `components` folder inside `app` folder. Then create inside it three files: `ContactsTableComponent.js` that will be our table, `ConctactRowComponent.js`, that will be a reusable row, and `ContactPropTypes.js` that will be the contact typechecking definition for our table components. Our folders tree should look like this: 56 | 57 | ``` 58 | . 59 | ├── assets 60 | │ ├── css 61 | │ │ └── styles.css 62 | │ └── js 63 | │ ├── app 64 | │ │ ├── App.js 65 | │ │ ├── components 66 | │ │ │ ├── ContactPropTypes.js 67 | │ │ │ ├── ContactRowComponent.js 68 | │ │ │ └── ContactsTableComponent.js 69 | │ │ ├── modules 70 | │ │ │ └── contactsModule.js 71 | │ │ └── services 72 | │ │ └── contactsService.js 73 | │ └── index.js 74 | └── index.html 75 | ``` 76 | 77 | ## Defining our components 78 | 79 | Let's define the validation for contact. This will need two main dependencies, `React` to use its typecheckers, and `App` to export the component so we'll define our component and wrap it in an _IIFE _(Immediately Invoked Function Expression), pass this dependencies and export the definition in `App.PropTypes` subnamespace. Our contact model will have three required attributes, a name as `string`, phone as `number` and `email` as string: 80 | 81 | ```javascript 82 | (function initializeContactPropTypes(React, App) { 83 | 'use strict'; 84 | 85 | var ContactPropTypes = PropTypes.shape({ 86 | name: PropTypes.string.isRequired, 87 | phone: PropTypes.number.isRequired, 88 | email: PropTypes.string.isRequired 89 | }); 90 | 91 | App.PropTypes.ContactPropTypes = ContactPropTypes; 92 | })(window.React, window.App); 93 | ``` 94 | 95 | Next let's write the `ContactRowComponent`. We'll wrap it inside an _IIFE_ with React and App as dependencies and create a stateless component using a function that receives a contact inside `props` parameter and returns a row with the contact information: 96 | 97 | ```javascript 98 | (function initializeContactRowComponent(React, App) { 99 | 'use strict'; 100 | 101 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 102 | 103 | var ContactRowComponent = function (props) { 104 | var contact = props.contact || {}; 105 | return React.createElement('tr', null, 106 | React.createElement('td', null, contact.name), 107 | React.createElement('td', null, contact.phone), 108 | React.createElement('td', null, contact.email) 109 | ) 110 | }; 111 | 112 | ContactRowComponent.displayName = 'ContactRowComponent'; 113 | ContactRowComponent.propTypes = { 114 | contact: ContactPropTypes 115 | }; 116 | 117 | App.components.ContactRowComponent = ContactRowComponent; 118 | })(window.React, window.App); 119 | ``` 120 | 121 | Then we'll create the `ContactsTableComponent`. It will need a contacts array from props to render the table: 122 | 123 | ```javascript 124 | (function initializeContactsTableComponent(React, App) { 125 | 'use strict'; 126 | 127 | var ContactRowComponent = App.components.ContactRowComponent; 128 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 129 | 130 | var ContactsTableComponent = function (props) { 131 | var contacts = props.contacts || []; 132 | return React.createElement( 133 | 'table', 134 | { className: 'table table-stripped table-bordered table-hover' }, 135 | React.createElement('thead', null, 136 | React.createElement('tr', null, 137 | React.createElement('th', null, 'Name'), 138 | React.createElement('th', null, 'Phone number'), 139 | React.createElement('th', null, 'Email') 140 | ) 141 | ), 142 | React.createElement('tbody', null, 143 | contacts.map(function (contact, index) { 144 | return React.createElement(ContactRowComponent, { 145 | contact: contact, 146 | key: index 147 | }); 148 | }) 149 | ) 150 | ); 151 | } 152 | 153 | ContactsTableComponent.displayName = 'ContactsTableComponent'; 154 | ContactsTableComponent.propTypes = { 155 | contacts: PropTypes.arrayOf(ContactPropTypes) 156 | }; 157 | 158 | App.components.ContactsTableComponent = ContactsTableComponent; 159 | })(React, window.App); 160 | ``` 161 | 162 | > As you can see, writing React components using ECMAScript 5 is a little bit verbose and confusing at indentation level because of `React.createElement`. In the next sample we will see how to integrate JSX in our project to enhace the readability of our component and speed the creation process: 163 | 164 | Finally let's include our contacts table files in our `index.html`: 165 | 166 | ```html 167 | ... 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | ``` 178 | 179 | ## Setting up communication 180 | 181 | Let's jump into our `contactsModule.js` and make some changes: 182 | 183 | - First we'll incude our `TableComponent` and import React and ReactDOM as dependencies. Then we'll remove the reference to `domUtils` and add the `TableComponent`: 184 | 185 | ```javascript 186 | (function initializeContactsModule($, React, ReactDOM, App) { 187 | 'use strict'; 188 | 189 | var ContactsTableComponent = App.components.ContactsTableComponent; 190 | var contactsService = App.contactsService; 191 | 192 | ... 193 | 194 | })(window.jQuery, window.React, window.ReactDOM, window.App); 195 | ``` 196 | - Then remove the `showContacts` method and replace its call by `ReactDOM.render` to mount our `TableComponent` component: 197 | 198 | ```javascript 199 | var fetchContacts = function () { 200 | $.when(contactsService.fetchContacts()) 201 | .then(function (fetchedContacts) { 202 | 203 | // Pass contacts to TableComponent 204 | ReactDOM.render( 205 | React.createElement(TableComponent, { contacts: fetchedContacts }), 206 | $('#tableComponent').get(0) 207 | ); 208 | }); 209 | }; 210 | 211 | var run = function () { 212 | ``` 213 | 214 | After all this changes, we should get our `TableComponent` integrated in our jQuery app. 215 | 216 | ## How it works? 217 | 218 | 1. When page loads `contactsModule.run` method is called requesting contacts data from `contactsService`. 219 | 220 | 2. When `fetchContacts` is completed `TableComponent` is mounted receiving fetched contacts transforms from `props` and transforming them into a table with rows containing each contact information. 221 | -------------------------------------------------------------------------------- /01.A Basic integration/assets/css/styles.css: -------------------------------------------------------------------------------- 1 | .header { 2 | margin-top: 50px; 3 | } 4 | 5 | .main { 6 | margin-top: 75px; 7 | } 8 | 9 | .form-inline .form-group > *:first-child { 10 | margin: 0 5px 0 20px; 11 | } 12 | 13 | table { 14 | table-layout: fixed; 15 | } 16 | 17 | table > thead > tr > th { 18 | background-color: #555; 19 | color: #FFF; 20 | } 21 | -------------------------------------------------------------------------------- /01.A Basic integration/assets/js/app/App.js: -------------------------------------------------------------------------------- 1 | (function initializeApp(window) { 2 | 'use strict'; 3 | 4 | var App = {}; 5 | App.components = {}; 6 | App.PropTypes = {}; 7 | 8 | window.App = App; 9 | })(window); 10 | -------------------------------------------------------------------------------- /01.A Basic integration/assets/js/app/components/ContactPropTypes.js: -------------------------------------------------------------------------------- 1 | (function initializeContactPropTypes(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = PropTypes.shape({ 5 | name: PropTypes.string.isRequired, 6 | phone: PropTypes.number.isRequired, 7 | email: PropTypes.string.isRequired 8 | }); 9 | 10 | App.PropTypes.ContactPropTypes = ContactPropTypes; 11 | })(window.React, window.App); 12 | -------------------------------------------------------------------------------- /01.A Basic integration/assets/js/app/components/ContactRowComponent.js: -------------------------------------------------------------------------------- 1 | (function initializeContactRowComponent(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | 6 | var ContactRowComponent = function (props) { 7 | var contact = props.contact || {}; 8 | return React.createElement('tr', null, 9 | React.createElement('td', null, contact.name), 10 | React.createElement('td', null, contact.phone), 11 | React.createElement('td', null, contact.email) 12 | ) 13 | }; 14 | 15 | ContactRowComponent.displayName = 'ContactRowComponent'; 16 | ContactRowComponent.propTypes = { 17 | contact: ContactPropTypes 18 | }; 19 | 20 | App.components.ContactRowComponent = ContactRowComponent; 21 | })(window.React, window.App); 22 | -------------------------------------------------------------------------------- /01.A Basic integration/assets/js/app/components/ContactsTableComponent.js: -------------------------------------------------------------------------------- 1 | (function initializeContactsTableComponent(React, App) { 2 | 'use strict'; 3 | 4 | var ContactRowComponent = App.components.ContactRowComponent; 5 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 6 | 7 | var ContactsTableComponent = function (props) { 8 | var contacts = props.contacts || []; 9 | return React.createElement( 10 | 'table', 11 | { className: 'table table-stripped table-bordered table-hover' }, 12 | React.createElement('thead', null, 13 | React.createElement('tr', null, 14 | React.createElement('th', null, 'Name'), 15 | React.createElement('th', null, 'Phone number'), 16 | React.createElement('th', null, 'Email') 17 | ) 18 | ), 19 | React.createElement('tbody', null, 20 | contacts.map(function (contact, index) { 21 | return React.createElement(ContactRowComponent, { 22 | contact: contact, 23 | key: index 24 | }); 25 | }) 26 | ) 27 | ); 28 | } 29 | 30 | ContactsTableComponent.displayName = 'ContactsTableComponent'; 31 | ContactsTableComponent.propTypes = { 32 | contacts: PropTypes.arrayOf(ContactPropTypes) 33 | }; 34 | 35 | App.components.ContactsTableComponent = ContactsTableComponent; 36 | })(window.React, window.App); 37 | -------------------------------------------------------------------------------- /01.A Basic integration/assets/js/app/modules/contactsModule.js: -------------------------------------------------------------------------------- 1 | (function initializeContactsModule($, React, ReactDOM, App) { 2 | 'use strict'; 3 | 4 | var ContactsTableComponent = App.components.ContactsTableComponent; 5 | var contactsService = App.contactsService; 6 | 7 | var contactsModule = (function () { 8 | 9 | // Simulates server call 10 | var fetchContacts = function () { 11 | $.when(contactsService.fetchContacts()) 12 | .then(function (fetchedContacts) { 13 | 14 | // Pass contacts to TableComponent 15 | ReactDOM.render( 16 | React.createElement(ContactsTableComponent, { contacts: fetchedContacts }), 17 | $('#tableComponent').get(0) 18 | ); 19 | }); 20 | }; 21 | 22 | var run = function () { 23 | fetchContacts(); 24 | }; 25 | 26 | return { 27 | run: run, 28 | }; 29 | })(); 30 | 31 | App.contactsModule = contactsModule; 32 | })(window.jQuery, window.React, window.ReactDOM, window.App); 33 | 34 | 35 | -------------------------------------------------------------------------------- /01.A Basic integration/assets/js/app/services/contactsService.js: -------------------------------------------------------------------------------- 1 | (function initializeContactsService($, App, window) { 2 | 'use strict'; 3 | 4 | var contactsService = (function () { 5 | var REQUEST_DELAY = 600; 6 | var contacts = [{ 7 | name: 'Jose C. Thomson', 8 | phone: 915797511, 9 | email: 'jcthomson@hotmail.com' 10 | }, 11 | { 12 | name: 'Shelia L. Clark', 13 | phone: 956631391, 14 | email: 'sheilac78@gmail.com' 15 | }, 16 | { 17 | name: 'Aaron B. Hudkins', 18 | phone: 660892268, 19 | email: 'flooreb@aol.com' 20 | }]; 21 | 22 | var fetchContacts = function () { 23 | var deferred = $.Deferred(); 24 | window.setTimeout(function () { 25 | deferred.resolve(contacts); 26 | }, REQUEST_DELAY); 27 | return deferred; 28 | }; 29 | 30 | return { 31 | fetchContacts: fetchContacts 32 | }; 33 | })(); 34 | 35 | App.contactsService = contactsService; 36 | })(window.jQuery, window.App, window) 37 | -------------------------------------------------------------------------------- /01.A Basic integration/assets/js/index.js: -------------------------------------------------------------------------------- 1 | (function initializeMain($, App) { 2 | 'use strict'; 3 | 4 | $(function main() { 5 | App.contactsModule.run(); 6 | }); 7 | })(window.jQuery, window.App); 8 | -------------------------------------------------------------------------------- /01.A Basic integration/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boilerplate 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Book Address

13 |
14 |
15 |

My contacts

16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /01.A Basic integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LegacyApps", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Lemoncode/integrate-react-legacy-apps.git", 6 | "author": "Santiago Camargo ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "bootstrap": "^3.3.7", 10 | "jquery": "^3.1.1", 11 | "prop-types": "^15.6.0", 12 | "react": "^16.2.0", 13 | "react-dom": "^16.2.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /01.B Move To JSX/README.md: -------------------------------------------------------------------------------- 1 | # 01.B Moving to JSX 2 | 3 | Most of React projects are written using JSX syntax with ECMAScript2015 and transpiled using Webpack, Rollup, Browserify or some other bundler. In this sample we'll be configuring our project to use a task manager to automatize our transpilation process. 4 | 5 | ## Why JSX? 6 | 7 | Using JSX syntax is not mandatory, however it has some advantages like better readabilty instead of using `React.createElement` method. JSX allows us to write components using fewer lines of code, and obtain a better visualization of them thanks to the HTML like syntax. 8 | 9 | ```jsx 10 | var createRow = function (contact, index) { 11 | return ( 12 | 13 | {contact.name} 14 | {contact.phone} 15 | {contact.email} 16 | 17 | ); 18 | }; 19 | ``` 20 | 21 | ## Set up workspace 22 | 23 | ## Installing dependencies 24 | 25 | We just checked how easy is to write JSX syntax, however, JSX is not supported by browsers so, to use it we need a middle step, that is _transpilation_. We need an interpreter that transforms JSX to JavaScript like [babel](https://babeljs.io). Babel has an built-in CLI which can be used to compile files from the command line. However when we make some changes to our `.jsx` files we need to use `babel` to transform them to `.js`. To automatize this and speed our development workflow we'll install a task manager that will run `babel` and _transpile_ our `.jsx` files when we make some changes. Let's install [Gulp 4](http://gulpjs.com), open a command line prompt, locate yourself in the root folder of the project and execute: 26 | 27 | ```shell 28 | npm install --save-dev gulpjs/gulp#4.0 gulp-babel babel-preset-react del 29 | ``` 30 | 31 | ### Project structure 32 | 33 | Now, we need to make a new folder that will contain the contents of our development code. Create a folder called `src` and move all of our assets there. 34 | 35 | Then, we need to make a folder that will contain files that we use for production purposes. We'll call this folder `dist`. Your project folder should looks like this: 36 | 37 | ``` 38 | . 39 | ├── dist/ 40 | ├── index.html 41 | ├── node_modules/ 42 | ├── package.json 43 | └── src/ 44 | ├── css/ 45 | │ └── styles.css 46 | └── js/ 47 | ├── app/ 48 | │ ├── App.js 49 | │ ├── components/ 50 | │ │ ├── ContactPropTypes.js 51 | │ │ ├── ContactRowComponent.js 52 | │ │ └── ContactsTableComponent.js 53 | │ ├── modules/ 54 | │ │ └── contactsModule.js 55 | │ └── services/ 56 | │ └── contactsService.js 57 | └── index.js 58 | ``` 59 | 60 | ### Gulp setup 61 | 62 | To configure Gulp let's create a `gulpfile.js` on the root folder of the project with the next content: 63 | 64 | ```javascript 65 | var path = require('path'); 66 | var gulp = require('gulp'); 67 | var babel = require('gulp-babel'); 68 | var del = require('del'); 69 | 70 | var BUILD_DIR = path.resolve(__dirname, 'src'); 71 | var DIST_DIR = path.resolve(__dirname, 'dist'); 72 | 73 | gulp.task('transpile', function () { 74 | return gulp 75 | .src(path.resolve(BUILD_DIR, '**', '*.jsx')) 76 | .pipe(babel({ 77 | presets: ['react'] 78 | })) 79 | .pipe(gulp.dest(DIST_DIR)); 80 | }); 81 | 82 | gulp.task('copy', function () { 83 | return gulp 84 | .src([ 85 | path.resolve(BUILD_DIR, '**', '*.css'), 86 | path.resolve(BUILD_DIR, '**', '*.js') 87 | ]) 88 | .pipe(gulp.dest(DIST_DIR)) 89 | }); 90 | 91 | gulp.task('clean', function () { 92 | return del(DIST_DIR); 93 | }); 94 | 95 | gulp.task('build', gulp.series('clean', gulp.parallel('copy', 'transpile'))); 96 | 97 | gulp.task('watch', function () { 98 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.css'), gulp.series('copy')); 99 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.js'), gulp.series('copy')); 100 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.jsx'), gulp.series('transpile')); 101 | }); 102 | 103 | gulp.task('default', gulp.parallel('build', 'watch')); 104 | ``` 105 | 106 | We've basically created six tasks: 107 | - `transpile` will transform all `.jsx` files into `.js` and move them to `dist` folder. 108 | - `copy` will make a copy of our JavaScript and CSS files and place them into the `dist` folder. 109 | - `clean` will erase all contents from our `dist` folder. 110 | - `build` will call `clean`, `copy` and `transpile` to create a fresh build. 111 | - `watch` will look out for changes made in our development files and move them to `dist` folder. If the file is `.jsx` then it _transpile_ it. 112 | - `default` will run `build` first time and `watch` to observe changes. 113 | 114 | Finally we'll add to our `package.json` a build task that call Gulp and start the development process: 115 | 116 | ```javascript 117 | ... 118 | "scripts": { 119 | "build": "gulp build", 120 | "build:watch": "gulp" 121 | }, 122 | "dependencies": { 123 | "bootstrap": "^3.3.7", 124 | ... 125 | ``` 126 | 127 | With these changes we only need to open a command line prompt and type `npm run build` to start our development process. 128 | 129 | ## JSX Implementation 130 | 131 | - Let's begin changing our `ContactRowComponent` to use JSX syntax. Rename it to `ContactRowComponent.jsx` and replace all of `React.createElement` statements with JSX syntax: 132 | 133 | ```jsx 134 | ... 135 | var ContactRowComponent = function (props) { 136 | var contact = props.contact || {}; 137 | return ( 138 | 139 | {contact.name} 140 | {contact.phone} 141 | {contact.email} 142 | 143 | ); 144 | }; 145 | 146 | ContactRowComponent.displayName = 'ContactRowComponent'; 147 | ContactRowComponent.propTypes = { 148 | contact: ContactPropTypes 149 | }; 150 | ... 151 | ``` 152 | 153 | This looks more familiar, right? 154 | 155 | - Next we'll proceed to change our `ContactsTableComponent` to use JSX syntax. Like we've done with`ContactRowComponent` let's rename it to `ContactsTableComponent.jsx` and replace all of `React.createElement` statements with JSX syntax : 156 | 157 | ```jsx 158 | var ContactsTableComponent = function (props) { 159 | var contacts = props.contacts || []; 160 | return ( 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | {contacts.map(function (contact, index) { 171 | return ; 172 | })} 173 | 174 |
NamePhone numberEmail
175 | ); 176 | }; 177 | ``` 178 | 179 | - Then we'll also rename our `contactsModule.js` to `contactsModule.jsx` and replace the ReactDOM.render arguments with: 180 | 181 | ```jsx 182 | // Pass contacts to TableComponent 183 | ReactDOM.render( 184 | , 185 | $('#tableComponent').get(0) 186 | ); 187 | ``` 188 | 189 | - With the new folder structure we need to make some changes in our `index.html` to use our `dist` folder instead of `assets`: 190 | 191 | ```html 192 | 193 | 194 | 195 | 196 | Boilerplate 197 | 198 | 199 | 200 | 201 |
202 |
203 |

Book Address

204 |
205 |
206 |

My contacts

207 |
208 |
209 |
210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | ``` 226 | 227 | - Finally let's make a build using Gulp, open a command line prompt, locate yourself in the root folder of the project and execute in a command prompt `npm run build` to get a copy of `src` files in `dist` folder without `.jsx` files. After running the build our `dist` folder should look like this: 228 | 229 | ``` 230 | dist 231 | ├── css 232 | │ └── styles.css 233 | └── js 234 | ├── app 235 | │ ├── App.js 236 | │ ├── components 237 | │ │ ├── ContactPropTypes.js 238 | │ │ ├── ContactRowComponent.js 239 | │ │ └── ContactsTableComponent.js 240 | │ ├── modules 241 | │ │ └── contactsModule.js 242 | │ └── services 243 | │ └── contactsService.js 244 | └── index.js 245 | ``` 246 | 247 | To see the example working open `index.html` in a browser. 248 | 249 | > If we were developing this component and making more changes we'll just run on the command line prompt `npm run build:watch` and Gulp will watch for changes and automatically transpile and copy the files to `dist` folder so you'll only have to refresh the browser to see changes. 250 | -------------------------------------------------------------------------------- /01.B Move To JSX/gulpfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var gulp = require('gulp'); 3 | var babel = require('gulp-babel'); 4 | var del = require('del'); 5 | 6 | var BUILD_DIR = path.resolve(__dirname, 'src'); 7 | var DIST_DIR = path.resolve(__dirname, 'dist'); 8 | 9 | gulp.task('transpile', function () { 10 | return gulp 11 | .src(path.resolve(BUILD_DIR, '**', '*.jsx')) 12 | .pipe(babel({ 13 | presets: ['react'] 14 | })) 15 | .pipe(gulp.dest(DIST_DIR)); 16 | }); 17 | 18 | gulp.task('copy', function () { 19 | return gulp 20 | .src([ 21 | path.resolve(BUILD_DIR, '**', '*.css'), 22 | path.resolve(BUILD_DIR, '**', '*.js') 23 | ]) 24 | .pipe(gulp.dest(DIST_DIR)) 25 | }); 26 | 27 | gulp.task('clean', function () { 28 | return del(DIST_DIR); 29 | }); 30 | 31 | gulp.task('build', gulp.series('clean', gulp.parallel('copy', 'transpile'))); 32 | 33 | gulp.task('watch', function () { 34 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.css'), gulp.series('copy')); 35 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.js'), gulp.series('copy')); 36 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.jsx'), gulp.series('transpile')); 37 | }); 38 | 39 | gulp.task('default', gulp.parallel('build', 'watch')); 40 | -------------------------------------------------------------------------------- /01.B Move To JSX/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boilerplate 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Book Address

13 |
14 |
15 |

My contacts

16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /01.B Move To JSX/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LegacyApps", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Lemoncode/integrate-react-legacy-apps.git", 6 | "author": "Santiago Camargo ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "gulp build", 10 | "build:watch": "gulp" 11 | }, 12 | "dependencies": { 13 | "bootstrap": "^3.3.7", 14 | "jquery": "^3.1.1", 15 | "prop-types": "^15.6.0", 16 | "react": "^16.2.0", 17 | "react-dom": "^16.2.0" 18 | }, 19 | "devDependencies": { 20 | "babel-preset-react": "^6.22.0", 21 | "del": "^2.2.2", 22 | "gulp": "gulpjs/gulp#4.0", 23 | "gulp-babel": "^6.1.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /01.B Move To JSX/src/css/styles.css: -------------------------------------------------------------------------------- 1 | .header { 2 | margin-top: 50px; 3 | } 4 | 5 | .main { 6 | margin-top: 75px; 7 | } 8 | 9 | .form-inline .form-group > *:first-child { 10 | margin: 0 5px 0 20px; 11 | } 12 | 13 | table { 14 | table-layout: fixed; 15 | } 16 | 17 | table > thead > tr > th { 18 | background-color: #555; 19 | color: #FFF; 20 | } 21 | -------------------------------------------------------------------------------- /01.B Move To JSX/src/js/app/App.js: -------------------------------------------------------------------------------- 1 | (function initializeApp(window) { 2 | 'use strict'; 3 | 4 | var App = {}; 5 | App.components = {}; 6 | App.PropTypes = {}; 7 | 8 | window.App = App; 9 | })(window); 10 | -------------------------------------------------------------------------------- /01.B Move To JSX/src/js/app/components/ContactPropTypes.js: -------------------------------------------------------------------------------- 1 | (function initializeContactPropTypes(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = PropTypes.shape({ 5 | name: PropTypes.string.isRequired, 6 | phone: PropTypes.number.isRequired, 7 | email: PropTypes.string.isRequired 8 | }); 9 | 10 | App.PropTypes.ContactPropTypes = ContactPropTypes; 11 | })(window.React, window.App); 12 | -------------------------------------------------------------------------------- /01.B Move To JSX/src/js/app/components/ContactRowComponent.jsx: -------------------------------------------------------------------------------- 1 | (function initializeContactRowComponent(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | 6 | var ContactRowComponent = function (props) { 7 | var contact = props.contact || {}; 8 | return ( 9 | 10 | {contact.name} 11 | {contact.phone} 12 | {contact.email} 13 | 14 | ); 15 | }; 16 | 17 | ContactRowComponent.displayName = 'ContactRowComponent'; 18 | ContactRowComponent.propTypes = { 19 | contact: ContactPropTypes 20 | }; 21 | 22 | App.components.ContactRowComponent = ContactRowComponent; 23 | })(window.React, window.App); 24 | -------------------------------------------------------------------------------- /01.B Move To JSX/src/js/app/components/ContactsTableComponent.jsx: -------------------------------------------------------------------------------- 1 | (function initializeTableComponent(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | var ContactRowComponent = App.components.ContactRowComponent; 6 | 7 | var ContactsTableComponent = function (props) { 8 | var contacts = props.contacts || []; 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {contacts.map(function (contact, index) { 20 | return ; 21 | })} 22 | 23 |
NamePhone numberEmail
24 | ); 25 | }; 26 | 27 | ContactsTableComponent.displayName = 'ContactsTableComponent'; 28 | ContactsTableComponent.propTypes = { 29 | contacts: PropTypes.arrayOf(ContactPropTypes) 30 | }; 31 | 32 | App.components.ContactsTableComponent = ContactsTableComponent; 33 | })(window.React, window.App); 34 | -------------------------------------------------------------------------------- /01.B Move To JSX/src/js/app/modules/contactsModule.jsx: -------------------------------------------------------------------------------- 1 | (function initializeContactsModule($, React, ReactDOM, App) { 2 | 'use strict'; 3 | 4 | var ContactsTableComponent = App.components.ContactsTableComponent; 5 | var contactsService = App.contactsService; 6 | 7 | var contactsModule = function () { 8 | 9 | // Simulates server call 10 | var fetchContacts = function () { 11 | $.when(contactsService.fetchContacts()).then(function (fetchedContacts) { 12 | 13 | // Pass contacts to TableComponent 14 | ReactDOM.render( 15 | , 16 | $('#tableComponent').get(0) 17 | ); 18 | }); 19 | }; 20 | 21 | var run = function () { 22 | fetchContacts(); 23 | }; 24 | 25 | return { 26 | run: run 27 | }; 28 | }(); 29 | 30 | App.contactsModule = contactsModule; 31 | })(window.jQuery, window.React, window.ReactDOM, window.App); 32 | -------------------------------------------------------------------------------- /01.B Move To JSX/src/js/app/services/contactsService.js: -------------------------------------------------------------------------------- 1 | (function initializeContactsService($, App, window) { 2 | 'use strict'; 3 | 4 | var contactsService = (function () { 5 | var REQUEST_DELAY = 600; 6 | var contacts = [{ 7 | name: 'Jose C. Thomson', 8 | phone: 915797511, 9 | email: 'jcthomson@hotmail.com' 10 | }, 11 | { 12 | name: 'Shelia L. Clark', 13 | phone: 956631391, 14 | email: 'sheilac78@gmail.com' 15 | }, 16 | { 17 | name: 'Aaron B. Hudkins', 18 | phone: 660892268, 19 | email: 'flooreb@aol.com' 20 | }]; 21 | 22 | var fetchContacts = function () { 23 | var deferred = $.Deferred(); 24 | window.setTimeout(function () { 25 | deferred.resolve(contacts); 26 | }, REQUEST_DELAY); 27 | return deferred; 28 | }; 29 | 30 | return { 31 | fetchContacts: fetchContacts 32 | }; 33 | })(); 34 | 35 | App.contactsService = contactsService; 36 | })(window.jQuery, window.App, window) 37 | -------------------------------------------------------------------------------- /01.B Move To JSX/src/js/index.js: -------------------------------------------------------------------------------- 1 | (function initializeMain($, App) { 2 | 'use strict'; 3 | 4 | $(function main() { 5 | App.contactsModule.run(); 6 | }); 7 | })(window.jQuery, window.App); 8 | -------------------------------------------------------------------------------- /02 Props and Render/README.md: -------------------------------------------------------------------------------- 1 | # 02 Props and Render 2 | 3 | 4 | In this sample we're going to add a form to add contacts and change our `Contacts` module to handle the form and refresh `ContactsTableComponent` with the new contacts. 5 | 6 | > You can start by opening a command prompt, locating yourself on the root folder of the project and executing `npm run build:watch` to tell Gulp to start watching for the changes done. 7 | 8 | ## Adding a contact form 9 | Let's begin by adding the form in our `index.hml` file right inside the `
`: 10 | 11 | ```html 12 | ... 13 |
14 |

Book Address

15 |

Add contact

16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 | ... 39 | ``` 40 | 41 | ## Libraries communication 42 | 43 | To use React inside jQuery we could create a custom method extended from `jQuery.prototype` called **react** that receives a component definition, some properties and an optional callback and return the mounted component. This is useful if we need to remount a component that receive new props. Let's create this `react` method in a new filed called `jquery-react.jsx` under a new folder `plugins`: 44 | 45 | ```jsx 46 | (function initializejQueryReactPlugin($, React, ReactDOM) { 47 | 'use strict'; 48 | 49 | $.fn.extend({ 50 | react: function (Component, props, callback) { 51 | var mountedComponent = ReactDOM.render( 52 | , 53 | this.get(0) 54 | ); 55 | 56 | if (typeof callback === 'function') { 57 | return callback(mountedComponent); 58 | } 59 | 60 | return mountedComponent; 61 | } 62 | }); 63 | })(window.jQuery, React, ReactDOM); 64 | ``` 65 | 66 | > Note that we added React as a dependency but we have not really used it. When we write `.jsx` files all `` get transformed into `React.createElement` statements so we need to add React. 67 | 68 | Then add jquery-react.js in our `index.html`file (remember `.jsx` will be _transpiled_ to `.js`): 69 | 70 | ```html 71 | ... 72 | 73 | 74 | 75 | 76 | 77 | ... 78 | ``` 79 | 80 | ## Refactoring `contactsModule` 81 | 82 | We need to do some changes to `contactsModule` to add event handling and storing the new contacts added so they can be passed to the `ContactsTableComponent` : 83 | 84 | - Let's begin renaming it to `contactsModule.js` as we will not be using JSX syntax here. Also remove `React` and `ReactDOM` from its dependencies: 85 | 86 | ```javascript 87 | (function initializeContactsModule($, App) { 88 | 'use strict'; 89 | 90 | ... 91 | 92 | App.contactsModule = contactsModule; 93 | })(window.jQuery, window.App); 94 | ``` 95 | 96 | - Now let's declare two variables, **contacts** to store all contacts and **$mountedContactsTableComponent** to keep cached our mounted component. 97 | 98 | ```javascript 99 | (function initializeContactsModule($, App) { 100 | 'use strict'; 101 | 102 | var ContactsTableComponent = App.components.ContactsTableComponent; 103 | var contactsService = App.contactsService; 104 | var contacts, $mountedContactsTableComponent; 105 | 106 | var contactsModule = (function () { 107 | ``` 108 | 109 | - Then we need to create a mehthod to add an `onSubmit` our new form, let's call it `createEventHandlers`: 110 | 111 | ```javascript 112 | ... 113 | var createEventHandlers = function () { 114 | $('#formContact').submit(onSubmit); 115 | }; 116 | 117 | // Simulates server call 118 | var fetchContacts = function () { 119 | ... 120 | ``` 121 | 122 | - Next let's create the `onSubmit` handler. This method will be intended to retrieve the form values, adapt it to a _contact_ model, add the contact model to our contacts array and finally tell our `ContactsTableComponent` to render: 123 | 124 | ```javascript 125 | var onSubmit = function (event) { 126 | event.preventDefault(); 127 | var form = event.currentTarget; 128 | 129 | // Retrieve data from form 130 | var contact = $(form) 131 | .serializeArray() 132 | .reduce(function (data, prop) { 133 | data[prop.name] = prop.value; 134 | return data; 135 | }, {}); 136 | 137 | // Insert contact 138 | addContact(getContactObject(contact)); 139 | 140 | // Reset form controls 141 | form.reset(); 142 | 143 | // Render table 144 | showContacts(contacts); 145 | }; 146 | ``` 147 | 148 | - Now we'll define a simple `addContact` that receives an `contact` objects and push it to `contacts` array using **immutable** methods like `Array.prototype.concat`. We'll also define a `getContactObject` to get the properties of the retrieved contact and map them to a `contact` model: 149 | 150 | ```javascript 151 | ... 152 | var getContactObject = function (contact) { 153 | return { 154 | name: contact.txtName || null, 155 | phone: parseInt(contact.txtPhone) || null, 156 | email: contact.txtEmail || null 157 | }; 158 | }; 159 | 160 | var addContact = function (contact) { 161 | contacts = contacts.concat([contact]); 162 | }; 163 | 164 | var onSubmit = function (event) { 165 | ... 166 | ``` 167 | 168 | - Next step is to define the `showContacts` method that will update our `ContactsTableComponent`: 169 | 170 | ```javascript 171 | .... 172 | var showContacts = function (contacts, callback) { 173 | $mountedContactsTableComponent.react(ContactsTableComponent, { contacts: contacts || [] }, callback); 174 | }; 175 | 176 | var getContactObject = function (contact) { 177 | .... 178 | ``` 179 | 180 | - Finally we'll create a `createReactComponents` method to initialize our component and store it in `$mountedContactsTableComponent` when `run` is called: 181 | 182 | ```javascript 183 | ... 184 | var createReactComponents = function () { 185 | $mountedContactsTableComponent = $('#ContactsTableComponent'); 186 | showContacts(null); 187 | }; 188 | 189 | var showContacts = function (contacts, callback) { 190 | ... 191 | 192 | var run = function () { 193 | fetchContacts(); 194 | createEventHandlers(); 195 | createReactComponents(); 196 | }; 197 | ``` 198 | 199 | ## How it works? 200 | 201 | 1. When page loads `contactsModule.run` method is called requesting contacts data from `contactsService`, setting the form `onSubmit` handler and initially mounting `ContactsTableComponent` with an empty array. 202 | 203 | 2. `ContactsTableComponent` jQuery selector is stored in `$mountedContactsTableComponent` variable and initially renders the table header. When `fetchContacts` is completed `ContactsTableComponent` is updated with new contacts (React diffs the current table with the new rendered table and renders only the ``). 204 | 205 | 3. When we fill the form and click on `Add contact` `onSubmit` is triggered retrieving the form data and storing the new contact in `contacts` array, then `contacts` is passed to `showContacts`, updateing the `ContactsTableComponent` and rendering last `` into the DOM. 206 | -------------------------------------------------------------------------------- /02 Props and Render/gulpfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var gulp = require('gulp'); 3 | var babel = require('gulp-babel'); 4 | var del = require('del'); 5 | 6 | var BUILD_DIR = path.resolve(__dirname, 'src'); 7 | var DIST_DIR = path.resolve(__dirname, 'dist'); 8 | 9 | gulp.task('transpile', function () { 10 | return gulp 11 | .src(path.resolve(BUILD_DIR, '**', '*.jsx')) 12 | .pipe(babel({ 13 | presets: ['react'] 14 | })) 15 | .pipe(gulp.dest(DIST_DIR)); 16 | }); 17 | 18 | gulp.task('copy', function () { 19 | return gulp 20 | .src([ 21 | path.resolve(BUILD_DIR, '**', '*.css'), 22 | path.resolve(BUILD_DIR, '**', '*.js') 23 | ]) 24 | .pipe(gulp.dest(DIST_DIR)) 25 | }); 26 | 27 | gulp.task('clean', function () { 28 | return del(DIST_DIR); 29 | }); 30 | 31 | gulp.task('build', gulp.series('clean', gulp.parallel('copy', 'transpile'))); 32 | 33 | gulp.task('watch', function () { 34 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.css'), gulp.series('copy')); 35 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.js'), gulp.series('copy')); 36 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.jsx'), gulp.series('transpile')); 37 | }); 38 | 39 | gulp.task('default', gulp.parallel('build', 'watch')); 40 | -------------------------------------------------------------------------------- /02 Props and Render/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boilerplate 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Add contact

13 |
14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |

My contacts

37 |
38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /02 Props and Render/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LegacyApps", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Lemoncode/integrate-react-legacy-apps.git", 6 | "author": "Santiago Camargo ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "gulp build", 10 | "build:watch": "gulp" 11 | }, 12 | "dependencies": { 13 | "bootstrap": "^3.3.7", 14 | "jquery": "^3.1.1", 15 | "prop-types": "^15.6.0", 16 | "react": "^16.2.0", 17 | "react-dom": "^16.2.0" 18 | }, 19 | "devDependencies": { 20 | "babel-preset-react": "^6.22.0", 21 | "del": "^2.2.2", 22 | "gulp": "gulpjs/gulp#4.0", 23 | "gulp-babel": "^6.1.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /02 Props and Render/src/css/styles.css: -------------------------------------------------------------------------------- 1 | .header { 2 | margin-top: 50px; 3 | } 4 | 5 | .main { 6 | margin-top: 75px; 7 | } 8 | 9 | .form-inline .form-group > *:first-child { 10 | margin: 0 5px 0 20px; 11 | } 12 | 13 | table { 14 | table-layout: fixed; 15 | } 16 | 17 | table > thead > tr > th { 18 | background-color: #555; 19 | color: #FFF; 20 | } 21 | -------------------------------------------------------------------------------- /02 Props and Render/src/js/app/App.js: -------------------------------------------------------------------------------- 1 | (function initializeApp(window) { 2 | 'use strict'; 3 | 4 | var App = {}; 5 | App.components = {}; 6 | App.PropTypes = {}; 7 | 8 | window.App = App; 9 | })(window); 10 | -------------------------------------------------------------------------------- /02 Props and Render/src/js/app/components/ContactPropTypes.js: -------------------------------------------------------------------------------- 1 | (function initializeContactPropTypes(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = PropTypes.shape({ 5 | name: PropTypes.string.isRequired, 6 | phone: PropTypes.number.isRequired, 7 | email: PropTypes.string.isRequired 8 | }); 9 | 10 | App.PropTypes.ContactPropTypes = ContactPropTypes; 11 | })(window.React, window.App); 12 | -------------------------------------------------------------------------------- /02 Props and Render/src/js/app/components/ContactRowComponent.jsx: -------------------------------------------------------------------------------- 1 | (function initializeContactRowComponent(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | 6 | var ContactRowComponent = function (props) { 7 | var contact = props.contact || {}; 8 | return ( 9 | 10 | {contact.name} 11 | {contact.phone} 12 | {contact.email} 13 | 14 | ); 15 | }; 16 | 17 | ContactRowComponent.displayName = 'ContactRowComponent'; 18 | ContactRowComponent.propTypes = { 19 | contact: ContactPropTypes 20 | }; 21 | 22 | App.components.ContactRowComponent = ContactRowComponent; 23 | })(window.React, window.App); 24 | -------------------------------------------------------------------------------- /02 Props and Render/src/js/app/components/ContactsTableComponent.jsx: -------------------------------------------------------------------------------- 1 | (function initializeTableComponent(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | var ContactRowComponent = App.components.ContactRowComponent; 6 | 7 | var ContactsTableComponent = function (props) { 8 | var contacts = props.contacts || []; 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {contacts.map(function (contact, index) { 20 | return ; 21 | })} 22 | 23 |
NamePhone numberEmail
24 | ); 25 | }; 26 | 27 | ContactsTableComponent.displayName = 'ContactsTableComponent'; 28 | ContactsTableComponent.propTypes = { 29 | contacts: PropTypes.arrayOf(ContactPropTypes) 30 | }; 31 | 32 | App.components.ContactsTableComponent = ContactsTableComponent; 33 | })(window.React, window.App); 34 | -------------------------------------------------------------------------------- /02 Props and Render/src/js/app/modules/contactsModule.js: -------------------------------------------------------------------------------- 1 | (function initializeContactsModule($, App) { 2 | 'use strict'; 3 | 4 | var ContactsTableComponent = App.components.ContactsTableComponent; 5 | var contactsService = App.contactsService; 6 | var contacts, $mountedContactsTableComponent; 7 | 8 | var contactsModule = (function () { 9 | 10 | 11 | var createReactComponents = function () { 12 | $mountedContactsTableComponent = $('#tableComponent'); 13 | showContacts(null); 14 | }; 15 | 16 | var showContacts = function (contacts, callback) { 17 | $mountedContactsTableComponent.react(ContactsTableComponent, { contacts: contacts || [] }, callback); 18 | }; 19 | 20 | var getContactObject = function (contact) { 21 | return { 22 | name: contact.txtName || null, 23 | phone: parseInt(contact.txtPhone) || null, 24 | email: contact.txtEmail || null 25 | }; 26 | }; 27 | 28 | var addContact = function (contact) { 29 | contacts = contacts.concat([contact]); 30 | }; 31 | 32 | var onSubmit = function (event) { 33 | event.preventDefault(); 34 | var form = event.currentTarget; 35 | 36 | // Retrieve data from form 37 | var contact = $(form) 38 | .serializeArray() 39 | .reduce(function (data, prop) { 40 | data[prop.name] = prop.value; 41 | return data; 42 | }, {}); 43 | 44 | // Insert contact 45 | addContact(getContactObject(contact)); 46 | 47 | // Reset form controls 48 | form.reset(); 49 | 50 | // Render table 51 | showContacts(contacts); 52 | }; 53 | 54 | var createEventHandlers = function () { 55 | $('#formContact').submit(onSubmit); 56 | }; 57 | 58 | // Simulates server call 59 | var fetchContacts = function () { 60 | $.when(contactsService.fetchContacts()) 61 | .then(function (fetchedContacts) { 62 | contacts = fetchedContacts; 63 | showContacts(contacts); 64 | }); 65 | }; 66 | 67 | var run = function () { 68 | fetchContacts(); 69 | createEventHandlers(); 70 | createReactComponents(); 71 | }; 72 | 73 | return { 74 | run: run, 75 | }; 76 | })(); 77 | 78 | App.contactsModule = contactsModule; 79 | })(window.jQuery, window.App); 80 | 81 | 82 | -------------------------------------------------------------------------------- /02 Props and Render/src/js/app/plugins/jquery-react.jsx: -------------------------------------------------------------------------------- 1 | (function initializejQueryReactPlugin($, React, ReactDOM) { 2 | 'use strict'; 3 | 4 | $.fn.extend({ 5 | react: function (Component, props, callback) { 6 | var mountedComponent = ReactDOM.render( 7 | , 8 | this.get(0) 9 | ); 10 | 11 | if (typeof callback === 'function') { 12 | return callback(mountedComponent); 13 | } 14 | 15 | return mountedComponent; 16 | } 17 | }); 18 | })(window.jQuery, window.React, window.ReactDOM); 19 | -------------------------------------------------------------------------------- /02 Props and Render/src/js/app/services/contactsService.js: -------------------------------------------------------------------------------- 1 | (function ($, App, window) { 2 | 'use strict'; 3 | 4 | var contactsService = (function () { 5 | var REQUEST_DELAY = 600; 6 | var contacts = [{ 7 | name: 'Jose C. Thomson', 8 | phone: 915797511, 9 | email: 'jcthomson@hotmail.com' 10 | }, 11 | { 12 | name: 'Shelia L. Clark', 13 | phone: 956631391, 14 | email: 'sheilac78@gmail.com' 15 | }, 16 | { 17 | name: 'Aaron B. Hudkins', 18 | phone: 660892268, 19 | email: 'flooreb@aol.com' 20 | }]; 21 | 22 | var fetchContacts = function () { 23 | var deferred = $.Deferred(); 24 | window.setTimeout(function () { 25 | deferred.resolve(contacts); 26 | }, REQUEST_DELAY); 27 | return deferred; 28 | }; 29 | 30 | return { 31 | fetchContacts: fetchContacts 32 | }; 33 | })(); 34 | 35 | App.contactsService = contactsService; 36 | })(window.jQuery, window.App, window) 37 | -------------------------------------------------------------------------------- /02 Props and Render/src/js/index.js: -------------------------------------------------------------------------------- 1 | (function initializeMain($, App) { 2 | 'use strict'; 3 | 4 | $(function main() { 5 | App.contactsModule.run(); 6 | }); 7 | })(window.jQuery, window.App); 8 | -------------------------------------------------------------------------------- /03 Stateful Component/README.md: -------------------------------------------------------------------------------- 1 | # 03 Stateful Component 2 | 3 | In this sample we're going to change the implementation of _02 Props and Render_ to introduce a wrapper component that can store contacts in its own state. 4 | 5 | Why wrap our `ContactsTableComponent` with other component instead of change it to a stateful component? 6 | 7 | The purpose of the new wrapper component is to be used as a bridge between your pure components and the application state. Little by little your application logic will be handled by React components with pieces of all of your application state. The purpose of the `ContactsTableComponent` is simply render a simple table given an array of contacts. If you need to manipulate the contacts or do more tasks (e.g. filtering contacts, pagination, etc) then it's better to use a container component to handle the logic and pass to the presentational component the data to be rendered. 8 | 9 | 10 | 11 | ## Using a container component 12 | 13 | We'll create a container component that will be used by our `contactsModule`. This component will use `ContactsTableComponent` to render the contacts. Let's call it `ContactsTableContainer`, it will be located under `components` folder. 14 | 15 | ```jsx 16 | (function initializeContactsTableContainer(React, App) { 17 | 'use strict'; 18 | 19 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 20 | var ContactsTableComponent = App.components.ContactsTableComponent; 21 | 22 | class ContactsTableContainer extends React.Component { 23 | 24 | constructor() { 25 | super(); 26 | this.state = { contacts: [] }; 27 | } 28 | 29 | render() { 30 | return ; 31 | } 32 | } 33 | 34 | App.components.ContactsTableContainer = ContactsTableContainer; 35 | })(window.React, window.App); 36 | ``` 37 | 38 | ## Setting component state 39 | 40 | There are few changes to do in `contactsModule`: 41 | 42 | - First we'll change `createReactComponents` method as we don't need to store the `#tableComponent` selector anymore so we'll store instead the mounted component: 43 | 44 | ```javascript 45 | var createReactComponents = function () { 46 | $mountedContactsTableContainer = $('#tableComponent').react(ContactsTableContainer, null); 47 | }; 48 | ``` 49 | 50 | - Then let's change `showContacts` method to use the component `setState` method to update its state that will trigger React lifecycles methods including `render`: 51 | 52 | ```javascript 53 | var showContacts = function (contacts, callback) { 54 | $mountedContactsTableContainer.setState({ contacts: contacts }); 55 | }; 56 | ``` 57 | 58 | ## How it works? 59 | 60 | 1. When page loads `contactsModule.run` method is called requesting contacts data from `contactsService`, setting the form `onSubmit` handler and initially mounting `ContactsTableContainer` which creates its own state with an empty array of contacts and renders our `ContactsTableComponent`. 61 | 62 | 2. `ContactsTableContainer` mounted component is stored in `$mountedContactsTableContainer` variable and initially renders the table header. When `fetchContacts` is completed `ContactsTableContainer` changes its state via `setState` with new contacts. This triggers React component lifecycles and results in rendering the `ContactsTableComponent` that will produce a `` with the contacts information. 63 | 64 | 3. When we fill the form and click on `Add contact` `onSubmit` is triggered retrieving the form data and storing the new contact in `contacts` array, then `contacts` is passed to `showContacts`, changing the `ContactsTableContainer` state so it renders last `` into the DOM. 65 | -------------------------------------------------------------------------------- /03 Stateful Component/gulpfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var gulp = require('gulp'); 3 | var babel = require('gulp-babel'); 4 | var del = require('del'); 5 | 6 | var BUILD_DIR = path.resolve(__dirname, 'src'); 7 | var DIST_DIR = path.resolve(__dirname, 'dist'); 8 | 9 | gulp.task('transpile', function () { 10 | return gulp 11 | .src(path.resolve(BUILD_DIR, '**', '*.jsx')) 12 | .pipe(babel({ 13 | presets: ['react'] 14 | })) 15 | .pipe(gulp.dest(DIST_DIR)); 16 | }); 17 | 18 | gulp.task('copy', function () { 19 | return gulp 20 | .src([ 21 | path.resolve(BUILD_DIR, '**', '*.css'), 22 | path.resolve(BUILD_DIR, '**', '*.js') 23 | ]) 24 | .pipe(gulp.dest(DIST_DIR)) 25 | }); 26 | 27 | gulp.task('clean', function () { 28 | return del(DIST_DIR); 29 | }); 30 | 31 | gulp.task('build', gulp.series('clean', gulp.parallel('copy', 'transpile'))); 32 | 33 | gulp.task('watch', function () { 34 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.css'), gulp.series('copy')); 35 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.js'), gulp.series('copy')); 36 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.jsx'), gulp.series('transpile')); 37 | }); 38 | 39 | gulp.task('default', gulp.parallel('build', 'watch')); 40 | -------------------------------------------------------------------------------- /03 Stateful Component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boilerplate 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Add contact

13 |
14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |

My contacts

37 |
38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /03 Stateful Component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LegacyApps", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Lemoncode/integrate-react-legacy-apps.git", 6 | "author": "Santiago Camargo ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "gulp build", 10 | "build:watch": "gulp" 11 | }, 12 | "dependencies": { 13 | "bootstrap": "^3.3.7", 14 | "jquery": "^3.1.1", 15 | "prop-types": "^15.6.0", 16 | "react": "^16.2.0", 17 | "react-dom": "^16.2.0" 18 | }, 19 | "devDependencies": { 20 | "babel-preset-react": "^6.22.0", 21 | "del": "^2.2.2", 22 | "gulp": "gulpjs/gulp#4.0", 23 | "gulp-babel": "^6.1.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /03 Stateful Component/src/css/styles.css: -------------------------------------------------------------------------------- 1 | .header { 2 | margin-top: 50px; 3 | } 4 | 5 | .main { 6 | margin-top: 75px; 7 | } 8 | 9 | .form-inline .form-group > *:first-child { 10 | margin: 0 5px 0 20px; 11 | } 12 | 13 | table { 14 | table-layout: fixed; 15 | } 16 | 17 | table > thead > tr > th { 18 | background-color: #555; 19 | color: #FFF; 20 | } 21 | -------------------------------------------------------------------------------- /03 Stateful Component/src/js/app/App.js: -------------------------------------------------------------------------------- 1 | (function initializeApp(window) { 2 | 'use strict'; 3 | 4 | var App = {}; 5 | App.components = {}; 6 | App.PropTypes = {}; 7 | 8 | window.App = App; 9 | })(window); 10 | -------------------------------------------------------------------------------- /03 Stateful Component/src/js/app/components/ContactPropTypes.js: -------------------------------------------------------------------------------- 1 | (function initializeContactPropTypes(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = PropTypes.shape({ 5 | name: PropTypes.string.isRequired, 6 | phone: PropTypes.number.isRequired, 7 | email: PropTypes.string.isRequired 8 | }); 9 | 10 | App.PropTypes.ContactPropTypes = ContactPropTypes; 11 | })(window.React, window.App); 12 | -------------------------------------------------------------------------------- /03 Stateful Component/src/js/app/components/ContactRowComponent.jsx: -------------------------------------------------------------------------------- 1 | (function initializeContactRowComponent(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | 6 | var ContactRowComponent = function (props) { 7 | var contact = props.contact || {}; 8 | return ( 9 | 10 | {contact.name} 11 | {contact.phone} 12 | {contact.email} 13 | 14 | ); 15 | }; 16 | 17 | ContactRowComponent.displayName = 'ContactRowComponent'; 18 | ContactRowComponent.propTypes = { 19 | contact: ContactPropTypes 20 | }; 21 | 22 | App.components.ContactRowComponent = ContactRowComponent; 23 | })(window.React, window.App); 24 | -------------------------------------------------------------------------------- /03 Stateful Component/src/js/app/components/ContactsTableComponent.jsx: -------------------------------------------------------------------------------- 1 | (function initializeContactsTableComponent(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | var ContactRowComponent = App.components.ContactRowComponent; 6 | 7 | var ContactsTableComponent = function (props) { 8 | var contacts = props.contacts || []; 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {contacts.map(function (contact, index) { 20 | return ; 21 | })} 22 | 23 |
NamePhone numberEmail
24 | ); 25 | }; 26 | 27 | ContactsTableComponent.displayName = 'ContactsTableComponent'; 28 | ContactsTableComponent.propTypes = { 29 | contacts: PropTypes.arrayOf(ContactPropTypes) 30 | }; 31 | 32 | App.components.ContactsTableComponent = ContactsTableComponent; 33 | })(window.React, window.App); 34 | -------------------------------------------------------------------------------- /03 Stateful Component/src/js/app/components/ContactsTableContainer.jsx: -------------------------------------------------------------------------------- 1 | (function initializeContactsTableContainer(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | var ContactsTableComponent = App.components.ContactsTableComponent; 6 | 7 | class ContactsTableContainer extends React.Component { 8 | 9 | constructor() { 10 | super(); 11 | this.state = { contacts: [] }; 12 | } 13 | 14 | render() { 15 | return ; 16 | } 17 | } 18 | 19 | App.components.ContactsTableContainer = ContactsTableContainer; 20 | })(window.React, window.App); 21 | -------------------------------------------------------------------------------- /03 Stateful Component/src/js/app/modules/contactsModule.js: -------------------------------------------------------------------------------- 1 | (function initializeContactsModule($, App) { 2 | 'use strict'; 3 | 4 | var ContactsTableContainer = App.components.ContactsTableContainer; 5 | var contactsService = App.contactsService; 6 | var contacts, $mountedContactsTableContainer; 7 | 8 | var contactsModule = (function () { 9 | 10 | var createReactComponents = function () { 11 | $mountedContactsTableContainer = $('#tableComponent').react(ContactsTableContainer, null); 12 | }; 13 | 14 | var showContacts = function (contacts, callback) { 15 | $mountedContactsTableContainer.setState({ contacts: contacts }); 16 | }; 17 | 18 | var getContactObject = function (contact) { 19 | return { 20 | name: contact.txtName || null, 21 | phone: parseInt(contact.txtPhone) || null, 22 | email: contact.txtEmail || null 23 | }; 24 | }; 25 | 26 | var addContact = function (contact) { 27 | contacts = contacts.concat(contact); 28 | }; 29 | 30 | var onSubmit = function (event) { 31 | event.preventDefault(); 32 | var form = event.currentTarget; 33 | 34 | // Retrieve data from form 35 | var contact = $(form) 36 | .serializeArray() 37 | .reduce(function (data, prop) { 38 | data[prop.name] = prop.value; 39 | return data; 40 | }, {}); 41 | 42 | // Insert contact 43 | addContact(getContactObject(contact)); 44 | 45 | // Reset form controls 46 | form.reset(); 47 | 48 | // Render table 49 | showContacts(contacts); 50 | }; 51 | 52 | var createEventHandlers = function () { 53 | $('#formContact').submit(onSubmit); 54 | }; 55 | 56 | // Simulates server call 57 | var fetchContacts = function () { 58 | $.when(contactsService.fetchContacts()) 59 | .then(function (fetchedContacts) { 60 | contacts = fetchedContacts; 61 | showContacts(contacts); 62 | }); 63 | }; 64 | 65 | var run = function () { 66 | fetchContacts(); 67 | createEventHandlers(); 68 | createReactComponents(); 69 | }; 70 | 71 | return { 72 | run: run, 73 | }; 74 | })(); 75 | 76 | App.contactsModule = contactsModule; 77 | })(window.jQuery, window.App); 78 | 79 | 80 | -------------------------------------------------------------------------------- /03 Stateful Component/src/js/app/plugins/jquery-react.jsx: -------------------------------------------------------------------------------- 1 | (function initializejQueryReactPlugin($, React, ReactDOM) { 2 | 'use strict'; 3 | 4 | $.fn.extend({ 5 | react: function (component, props, callback) { 6 | var mountedComponent = ReactDOM.render( 7 | React.createElement(component, props), 8 | this.get(0) 9 | ); 10 | 11 | if (typeof callback === 'function') { 12 | return callback(mountedComponent); 13 | } 14 | 15 | return mountedComponent; 16 | } 17 | }); 18 | })(window.jQuery, window.React, window.ReactDOM); 19 | -------------------------------------------------------------------------------- /03 Stateful Component/src/js/app/services/contactsService.js: -------------------------------------------------------------------------------- 1 | (function initializeContactsService($, App, window) { 2 | 'use strict'; 3 | 4 | var contactsService = (function () { 5 | var REQUEST_DELAY = 600; 6 | var contacts = [{ 7 | name: 'Jose C. Thomson', 8 | phone: 915797511, 9 | email: 'jcthomson@hotmail.com' 10 | }, 11 | { 12 | name: 'Shelia L. Clark', 13 | phone: 956631391, 14 | email: 'sheilac78@gmail.com' 15 | }, 16 | { 17 | name: 'Aaron B. Hudkins', 18 | phone: 660892268, 19 | email: 'flooreb@aol.com' 20 | }]; 21 | 22 | var fetchContacts = function () { 23 | var deferred = $.Deferred(); 24 | window.setTimeout(function () { 25 | deferred.resolve(contacts); 26 | }, REQUEST_DELAY); 27 | return deferred; 28 | }; 29 | 30 | return { 31 | fetchContacts: fetchContacts 32 | }; 33 | })(); 34 | 35 | App.contactsService = contactsService; 36 | })(window.jQuery, window.App, window) 37 | -------------------------------------------------------------------------------- /03 Stateful Component/src/js/index.js: -------------------------------------------------------------------------------- 1 | (function initializeMain($, App) { 2 | 'use strict'; 3 | 4 | $(function main() { 5 | App.contactsModule.run(); 6 | }); 7 | })(window.jQuery, window.App); 8 | -------------------------------------------------------------------------------- /04 Event Emitters/README.md: -------------------------------------------------------------------------------- 1 | # 04 Event Emitters 2 | 3 | In this sample we're going to use the [Publish/Subscribe pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) using [jQuery.Callbacks](https://api.jquery.com/jQuery.Callbacks/) method. This implementation is useful if we need a module or component that needs to listen external changes through events to do some actions. We gain loose coupling, methods that have one unique responsability and good scalability. 4 | 5 | This sample takes as starting point sample _03 Stateful Components_. 6 | 7 | ## Creating subjects 8 | 9 | - First we'll create under `plugins` folder a new file called `jquery-pub-sub.js` and remove `jquery-react.jsx` as we aren't going to need it. Your folder structure should look like this: 10 | 11 | ``` 12 | . 13 | ├─ gulpfile.js 14 | ├─ index.html 15 | ├─ package.json 16 | ├─ node_modules/ 17 | └── src/ 18 | ├── css/ 19 | │ └── styles.css 20 | └── js/ 21 | ├── app/ 22 | │ ├── App.js 23 | │ ├── components/ 24 | │ │ ├── ContactPropTypes.jsx 25 | │ │ ├── ContactRowComponent.jsx 26 | │ │ ├── ContactsTableComponent.jsx 27 | │ │ └── ContactsTableContainer.jsx 28 | │ ├── modules/ 29 | │ │ └── contactsModule.jsx 30 | │ ├── plugins/ 31 | │ │ └── jquery-pub-sub.js 32 | │ └── services/ 33 | │ └── contactsService.js 34 | └── index.js 35 | ``` 36 | 37 | - Then let's define our Pub/Sub implementation, we'll create an object containing all subjects and wrap `jQuery.Callbacks()` when subject don't exist: 38 | 39 | ```javascript 40 | (function initializejQueryPubSub($) { 41 | $.observe = (function () { 42 | var subjects = {}; 43 | return function (id) { 44 | var callbacks, method; 45 | var subject = id && subjects[id]; 46 | 47 | if (!subject) { 48 | callbacks = $.Callbacks(); 49 | subject = { 50 | publish: callbacks.fire, 51 | subscribe: callbacks.add, 52 | unsubscribe: callbacks.remove 53 | }; 54 | 55 | if (id) { 56 | subjects[id] = subject; 57 | } 58 | } 59 | return subject; 60 | }; 61 | })(); 62 | })(window.jQuery); 63 | ``` 64 | 65 | - Then we can add `jquery-pub-sub.js` to our `index.html` file: 66 | 67 | ```html 68 | ... 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ... 77 | ``` 78 | 79 | ## Publishing contact 80 | 81 | Let's change the `contactsModule.js` implementation to use Pub/Sub pattern: 82 | 83 | - First, we'll rename it to `contactsModule.jsx` add React and ReactDOM to our module dependencies: 84 | 85 | ```javascript 86 | (function initializeContactsModule($, React, ReactDOM, App) { 87 | 'use strict'; 88 | 89 | var ContactsTableContainer = App.components.ContactsTableContainer; 90 | 91 | ... 92 | 93 | App.contactsModule = contactsModule; 94 | })(window.jQuery, window.React, window.ReactDOM, window.App); 95 | ``` 96 | 97 | - Next we'll remove `$mountedContactsTableContainer` variable and change `createReactComponents` method definition: 98 | 99 | ```jsx 100 | ... 101 | var contactsService = App.contactsService; 102 | var contacts; 103 | 104 | var contactsModule = (function () { 105 | 106 | var createReactComponents = function () { 107 | ReactDOM.render( 108 | , 109 | $('#tableComponent').get(0) 110 | ); 111 | }; 112 | ... 113 | ``` 114 | 115 | - We'll also remove `showContacts` method. We'll create a _subject_ for new contacts notifications called `contacts` that will be used to pass new contacts so let's change `onSubmit` method definition: 116 | 117 | ```javascript 118 | ... 119 | var onSubmit = function (event) { 120 | event.preventDefault(); 121 | var form = event.currentTarget; 122 | 123 | // Retrieve data from form 124 | var contact= $(form) 125 | .serializeArray() 126 | .reduce(function (data, prop) { 127 | data[prop.name] = prop.value; 128 | return data; 129 | }, {}); 130 | 131 | // Insert contact 132 | contact = getContactObject(contact); 133 | addContact(contact); 134 | 135 | // Reset form controls 136 | form.reset(); 137 | 138 | // Fire notification with new contact 139 | $.observe('addContacts').publish(contact); 140 | }; 141 | ... 142 | ``` 143 | 144 | - Next we'll change `fetchContacts` definition to also use `$.observe`. This time we will publish all new contacts at once: 145 | 146 | ```javascript 147 | ... 148 | // Simulates server call 149 | var fetchContacts = function () { 150 | $.when(contactsService.fetchContacts()) 151 | .then(function (fetchedContacts) { 152 | contacts = fetchedContacts; 153 | $.observe('addContacts').publish(contacts); 154 | }); 155 | }; 156 | ... 157 | ``` 158 | 159 | ## Subscribing our `ContactsTableContainer` to subject 160 | 161 | Subscribing our `ContactsTableContainer` to a subject is pretty straightforward: 162 | 163 | - First we'll need to add `jQuery` to its dependencies: 164 | 165 | ```javascript 166 | (function initializeContactsTableContainer($, React, App) { 167 | 'use strict'; 168 | 169 | ... 170 | 171 | App.components.ContactsTableContainer = ContactsTableContainer; 172 | })(window.jQuery, window.React, window.App); 173 | ``` 174 | 175 | - Next we'll create a method to handle new contacts through subscription to `addContacts` subject and set it in its state: 176 | 177 | ```javascript 178 | ... 179 | class ContactsTableContainer extends React.Component { 180 | 181 | constructor() { 182 | super(); 183 | this.state = { contacts: [] }; 184 | this.onAddContact = this.onAddContact.bind(this); 185 | } 186 | 187 | onAddContact(contact) { 188 | this.setState({ 189 | contacts: this.state.contacts.concat(contact) 190 | }); 191 | } 192 | ... 193 | ``` 194 | 195 | - Finally we need to define two React lifecycles methods, with `componentDidMount` we'll subscribe to `addContacts` subject and `componentWillUnmount` will be used to unsubscribe the component from the subject: 196 | 197 | ```javascript 198 | ... 199 | componentDidMount() { 200 | $.observe('addContacts').subscribe(this.onAddContact); 201 | } 202 | 203 | componentWillUnmount() { 204 | $.observe('addContacts').unsubscribe(this.onAddContact); 205 | } 206 | 207 | render() { 208 | ... 209 | ``` 210 | 211 | ## How it works? 212 | 213 | 1. When page loads `contactsModule.run` method is called requesting contacts data from `contactsService`, setting the form `onSubmit` handler and initially mounting `ContactsTableComponent` which creates its own state with an empty array of contacts and renders the table header. 214 | 215 | 2. When `fetchContacts` is completed we fire the subject `addContacts` with the new contacts and `ContactsTableComponent` gets notified, calling `onAddContact` method that changes its state via `setState` with new contacts. This triggers React component lifecycles and results in rendering `` with the contacts information. 216 | 217 | 3. When we fill the form and click on `Add contact` `onSubmit` is triggered retrieving the form data and storing the new contact in `contacts` array, then `addContacs` subject is fired with the new contact and `ContactsTableComponent` receives the notification with the new contact so it pushes into its state with the rest of contacts. React diffs the new state and it renders last `` into the DOM. 218 | 219 | -------------------------------------------------------------------------------- /04 Event Emitters/gulpfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var gulp = require('gulp'); 3 | var babel = require('gulp-babel'); 4 | var del = require('del'); 5 | 6 | var BUILD_DIR = path.resolve(__dirname, 'src'); 7 | var DIST_DIR = path.resolve(__dirname, 'dist'); 8 | 9 | gulp.task('transpile', function () { 10 | return gulp 11 | .src(path.resolve(BUILD_DIR, '**', '*.jsx')) 12 | .pipe(babel({ 13 | presets: ['react'] 14 | })) 15 | .pipe(gulp.dest(DIST_DIR)); 16 | }); 17 | 18 | gulp.task('copy', function () { 19 | return gulp 20 | .src([ 21 | path.resolve(BUILD_DIR, '**', '*.css'), 22 | path.resolve(BUILD_DIR, '**', '*.js') 23 | ]) 24 | .pipe(gulp.dest(DIST_DIR)) 25 | }); 26 | 27 | gulp.task('clean', function () { 28 | return del(DIST_DIR); 29 | }); 30 | 31 | gulp.task('build', gulp.series('clean', gulp.parallel('copy', 'transpile'))); 32 | 33 | gulp.task('watch', function () { 34 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.css'), gulp.series('copy')); 35 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.js'), gulp.series('copy')); 36 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.jsx'), gulp.series('transpile')); 37 | }); 38 | 39 | gulp.task('default', gulp.parallel('build', 'watch')); 40 | -------------------------------------------------------------------------------- /04 Event Emitters/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boilerplate 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Add contact

13 |
14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |

My contacts

37 |
38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /04 Event Emitters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LegacyApps", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Lemoncode/integrate-react-legacy-apps.git", 6 | "author": "Santiago Camargo ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "gulp build", 10 | "build:watch": "gulp" 11 | }, 12 | "dependencies": { 13 | "bootstrap": "^3.3.7", 14 | "jquery": "^3.1.1", 15 | "prop-types": "^15.6.0", 16 | "react": "^16.2.0", 17 | "react-dom": "^16.2.0" 18 | }, 19 | "devDependencies": { 20 | "babel-preset-react": "^6.22.0", 21 | "del": "^2.2.2", 22 | "gulp": "gulpjs/gulp#4.0", 23 | "gulp-babel": "^6.1.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /04 Event Emitters/src/css/styles.css: -------------------------------------------------------------------------------- 1 | .header { 2 | margin-top: 50px; 3 | } 4 | 5 | .main { 6 | margin-top: 75px; 7 | } 8 | 9 | .form-inline .form-group > *:first-child { 10 | margin: 0 5px 0 20px; 11 | } 12 | 13 | table { 14 | table-layout: fixed; 15 | } 16 | 17 | table > thead > tr > th { 18 | background-color: #555; 19 | color: #FFF; 20 | } 21 | -------------------------------------------------------------------------------- /04 Event Emitters/src/js/app/App.js: -------------------------------------------------------------------------------- 1 | (function initializeApp(window) { 2 | 'use strict'; 3 | 4 | var App = {}; 5 | App.components = {}; 6 | App.PropTypes = {}; 7 | 8 | window.App = App; 9 | })(window); 10 | -------------------------------------------------------------------------------- /04 Event Emitters/src/js/app/components/ContactPropTypes.js: -------------------------------------------------------------------------------- 1 | (function initializeContactPropTypes(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = PropTypes.shape({ 5 | name: PropTypes.string.isRequired, 6 | phone: PropTypes.number.isRequired, 7 | email: PropTypes.string.isRequired 8 | }); 9 | 10 | App.PropTypes.ContactPropTypes = ContactPropTypes; 11 | })(window.React, window.App); 12 | -------------------------------------------------------------------------------- /04 Event Emitters/src/js/app/components/ContactRowComponent.jsx: -------------------------------------------------------------------------------- 1 | (function initializeContactRowComponent(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | 6 | var ContactRowComponent = function (props) { 7 | var contact = props.contact || {}; 8 | return ( 9 | 10 | {contact.name} 11 | {contact.phone} 12 | {contact.email} 13 | 14 | ); 15 | }; 16 | 17 | ContactRowComponent.displayName = 'ContactRowComponent'; 18 | ContactRowComponent.propTypes = { 19 | contact: ContactPropTypes 20 | }; 21 | 22 | App.components.ContactRowComponent = ContactRowComponent; 23 | })(window.React, window.App); 24 | -------------------------------------------------------------------------------- /04 Event Emitters/src/js/app/components/ContactsTableComponent.jsx: -------------------------------------------------------------------------------- 1 | (function initializeContactsTableComponent(React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | var ContactRowComponent = App.components.ContactRowComponent; 6 | 7 | var ContactsTableComponent = function (props) { 8 | var contacts = props.contacts || []; 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {contacts.map(function (contact, index) { 20 | return ; 21 | })} 22 | 23 |
NamePhone numberEmail
24 | ); 25 | }; 26 | 27 | ContactsTableComponent.displayName = 'ContactsTableComponent'; 28 | ContactsTableComponent.propTypes = { 29 | contacts: PropTypes.arrayOf(ContactPropTypes) 30 | }; 31 | 32 | App.components.ContactsTableComponent = ContactsTableComponent; 33 | })(window.React, window.App); 34 | -------------------------------------------------------------------------------- /04 Event Emitters/src/js/app/components/ContactsTableContainer.jsx: -------------------------------------------------------------------------------- 1 | (function initializeContactsTableContainer($, React, App) { 2 | 'use strict'; 3 | 4 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 5 | var ContactsTableComponent = App.components.ContactsTableComponent; 6 | 7 | class ContactsTableContainer extends React.Component { 8 | 9 | constructor() { 10 | super(); 11 | this.state = { contacts: [] }; 12 | this.onAddContact = this.onAddContact.bind(this); 13 | } 14 | 15 | onAddContact(contact) { 16 | this.setState({ 17 | contacts: this.state.contacts.concat(contact) 18 | }); 19 | } 20 | 21 | componentDidMount() { 22 | $.observe('addContacts').subscribe(this.onAddContact); 23 | } 24 | 25 | componentWillUnmount() { 26 | $.observe('addContacts').unsubscribe(this.onAddContact); 27 | } 28 | 29 | render() { 30 | return ; 31 | } 32 | } 33 | 34 | App.components.ContactsTableContainer = ContactsTableContainer; 35 | })(window.jQuery, window.React, window.App); 36 | -------------------------------------------------------------------------------- /04 Event Emitters/src/js/app/modules/contactsModule.jsx: -------------------------------------------------------------------------------- 1 | (function initializeContactsModule($, React, ReactDOM, App) { 2 | 'use strict'; 3 | 4 | var ContactsTableContainer = App.components.ContactsTableContainer; 5 | var contactsService = App.contactsService; 6 | var contacts; 7 | 8 | var contactsModule = (function () { 9 | 10 | var createReactComponents = function () { 11 | ReactDOM.render( 12 | , 13 | $('#tableComponent').get(0) 14 | ); 15 | }; 16 | 17 | var getContactObject = function (contact) { 18 | return { 19 | name: contact.txtName || null, 20 | phone: parseInt(contact.txtPhone) || null, 21 | email: contact.txtEmail || null 22 | }; 23 | }; 24 | 25 | var addContact = function (contact) { 26 | contacts = contacts.concat(contact); 27 | }; 28 | 29 | var onSubmit = function (event) { 30 | event.preventDefault(); 31 | var form = event.currentTarget; 32 | 33 | // Retrieve data from form 34 | var contact = $(form) 35 | .serializeArray() 36 | .reduce(function (data, prop) { 37 | data[prop.name] = prop.value; 38 | return data; 39 | }, {}); 40 | 41 | // Insert contact 42 | contact = getContactObject(contact); 43 | addContact(contact); 44 | 45 | // Reset form controls 46 | form.reset(); 47 | 48 | // Fire notification with new contact 49 | $.observe('addContacts').publish(contact); 50 | }; 51 | 52 | var createEventHandlers = function () { 53 | $('#formContact').submit(onSubmit); 54 | }; 55 | 56 | // Simulates server call 57 | var fetchContacts = function () { 58 | $.when(contactsService.fetchContacts()) 59 | .then(function (fetchedContacts) { 60 | contacts = fetchedContacts; 61 | $.observe('addContacts').publish(contacts); 62 | }); 63 | }; 64 | 65 | var run = function () { 66 | fetchContacts(); 67 | createEventHandlers(); 68 | createReactComponents(); 69 | }; 70 | 71 | return { 72 | run: run, 73 | }; 74 | })(); 75 | 76 | App.contactsModule = contactsModule; 77 | })(window.jQuery, window.React, window.ReactDOM, window.App); 78 | 79 | 80 | -------------------------------------------------------------------------------- /04 Event Emitters/src/js/app/plugins/jquery-pub-sub.js: -------------------------------------------------------------------------------- 1 | (function initializejQueryPubSub($) { 2 | $.observe = (function () { 3 | var subjects = {}; 4 | return function (id) { 5 | var callbacks; 6 | var subject = id && subjects[id]; 7 | 8 | if (!subject) { 9 | callbacks = $.Callbacks(); 10 | subject = { 11 | publish: callbacks.fire, 12 | subscribe: callbacks.add, 13 | unsubscribe: callbacks.remove 14 | }; 15 | 16 | if (id) { 17 | subjects[id] = subject; 18 | } 19 | } 20 | return subject; 21 | }; 22 | })(); 23 | })(window.jQuery); 24 | -------------------------------------------------------------------------------- /04 Event Emitters/src/js/app/services/contactsService.js: -------------------------------------------------------------------------------- 1 | (function initializeContactsService($, App, window) { 2 | 'use strict'; 3 | 4 | var contactsService = (function () { 5 | var REQUEST_DELAY = 600; 6 | var contacts = [{ 7 | name: 'Jose C. Thomson', 8 | phone: 915797511, 9 | email: 'jcthomson@hotmail.com' 10 | }, 11 | { 12 | name: 'Shelia L. Clark', 13 | phone: 956631391, 14 | email: 'sheilac78@gmail.com' 15 | }, 16 | { 17 | name: 'Aaron B. Hudkins', 18 | phone: 660892268, 19 | email: 'flooreb@aol.com' 20 | }]; 21 | 22 | var fetchContacts = function () { 23 | var deferred = $.Deferred(); 24 | window.setTimeout(function () { 25 | deferred.resolve(contacts); 26 | }, REQUEST_DELAY); 27 | return deferred; 28 | }; 29 | 30 | return { 31 | fetchContacts: fetchContacts 32 | }; 33 | })(); 34 | 35 | App.contactsService = contactsService; 36 | })(window.jQuery, window.App, window) 37 | -------------------------------------------------------------------------------- /04 Event Emitters/src/js/index.js: -------------------------------------------------------------------------------- 1 | (function initializeMain($, App) { 2 | 'use strict'; 3 | 4 | $(function main() { 5 | App.contactsModule.run(); 6 | }); 7 | })(window.jQuery, window.App); 8 | -------------------------------------------------------------------------------- /05.A Angular controllerAS & directive/README.md: -------------------------------------------------------------------------------- 1 | # Angular 1.X integration: controllerAs + react-component 2 | 3 | ## Description 4 | 5 | This sample is one of the four _AngularJS integration_ samples that shows how to integrate React components in an existing AngularJS application. 6 | This particular sample uses a simple form created handled by a controller using _controllerAs_ syntax and _react-component_ directive. 7 | 8 | ## Boilerplate 9 | 10 | We'll be using JSX syntax so we will reuse the same Gulp configuration and package.json from sample [04 Event Emitters](../04\ Event\ Emitters). 11 | 12 | - First let's add AngularJS: 13 | 14 | ```shell 15 | npm install --save angular@^1.6.1 16 | ``` 17 | 18 | - Next let's write an `index.html` with a simple form that ask the user for a text, encodes a text into base64 and shows it in a well. We'll add `ng-app="app"`, `ng-controller="Main as ctrl"` directives: 19 | 20 | ```html 21 | 22 | 23 | 24 | 25 | 05.A Angular integration 26 | 27 | 28 | 29 | 30 | 31 |
32 |

controllerAS syntax + react-component

33 |
34 |
35 |
36 |
37 |
38 |

Base64 encoder

39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 |

49 | Encoded text 50 |

51 |
{{ctrl.encodedText || 'Nothing written'}}
52 |
53 |
54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 | ``` 62 | 63 | - Next let's add the `app` module with the next content (we will write our assets under `src` folder): 64 | 65 | ```javascript 66 | (function (angular) { 67 | 'use strict'; 68 | 69 | angular.module('app', []); 70 | })(window.angular); 71 | ``` 72 | 73 | - Then let's create the `main` controller that stores the text and its encoded version: 74 | 75 | ```javascript 76 | (function (angular) { 77 | 'use strict'; 78 | 79 | function Main() { 80 | var self = this; 81 | self.text = ''; 82 | self.encodedText = ''; 83 | self.encode = function (event) { 84 | event.preventDefault(); 85 | self.encodedText = btoa(self.text); 86 | }; 87 | } 88 | angular.module('app').controller('Main', [Main]); 89 | })(window.angular); 90 | ``` 91 | 92 | We'll end with this page: 93 | 94 | ![Boilerplate](../99_readme_resources/00boilerplate.gif "Boilerplate") 95 | 96 | ## React injection 97 | 98 | It's time to add React and our first component. 99 | 100 | - First let's install `React`, `ReactDOM` and [ngReact](https://github.com/ngReact/ngReact): 101 | 102 | ```shell 103 | npm install --save react react-dom ngreact 104 | ``` 105 | 106 | - Next let's create our first component under a new `components` folder that will replace the view for the encoded text. We will call it `ShowEncoded.jsx`: 107 | 108 | ```jsx 109 | (function (angular, React) { 110 | 'use strict'; 111 | 112 | var ShowEncoded = function (props) { 113 | return ( 114 |
115 |

116 | Encoded text 117 |

118 |
{props.encoded || 'Nothing written'}
119 |
120 | ); 121 | }; 122 | 123 | ShowEncoded.propTypes = { 124 | encoded: PropTypes.string.isRequired 125 | }; 126 | 127 | angular.module('app').value('ShowEncoded', ShowEncoded); 128 | })(window.angular, window.React); 129 | ``` 130 | 131 | This component will receive an `encoded` string property and will show it in a `
` tag.
132 | 
133 | - Next let's replace the view with our `ShowEncoded` component in `index.html`:
134 | 
135 | ```diff
136 |           
137 |           
138 | + 142 | -

143 | - Encoded text 144 | -

145 | -
{{ctrl.encodedText || 'Nothing written'}}
146 | -
147 | 148 | 149 | 150 | 151 | + 152 | + 153 | + 154 | + 155 | 156 | 157 | + 158 | 159 | 160 | ``` 161 | Finally let's add `react` from `ngReact` as a dependency for our `app` module: 162 | 163 | ```diff 164 | (function (angular) { 165 | 'use strict'; 166 | 167 | - angular.module('app', []); 168 | + angular.module('app', ['react']); 169 | })(window.angular); 170 | ``` 171 | 172 | We've implemented our first component using `controlerAs` syntax and `react-component` directive. In the next sample we'll see how to implement the same sample using `reactDirective` factory. 173 | -------------------------------------------------------------------------------- /05.A Angular controllerAS & directive/gulpfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var gulp = require('gulp'); 3 | var babel = require('gulp-babel'); 4 | var del = require('del'); 5 | 6 | var BUILD_DIR = path.resolve(__dirname, 'src'); 7 | var DIST_DIR = path.resolve(__dirname, 'dist'); 8 | 9 | gulp.task('transpile', function () { 10 | return gulp 11 | .src(path.resolve(BUILD_DIR, '**', '*.jsx')) 12 | .pipe(babel({ 13 | presets: ['react'] 14 | })) 15 | .pipe(gulp.dest(DIST_DIR)); 16 | }); 17 | 18 | gulp.task('copy', function () { 19 | return gulp 20 | .src([ 21 | path.resolve(BUILD_DIR, '**', '*.css'), 22 | path.resolve(BUILD_DIR, '**', '*.js') 23 | ]) 24 | .pipe(gulp.dest(DIST_DIR)) 25 | }); 26 | 27 | gulp.task('clean', function () { 28 | return del(DIST_DIR); 29 | }); 30 | 31 | gulp.task('build', gulp.series('clean', gulp.parallel('copy', 'transpile'))); 32 | 33 | gulp.task('watch', function () { 34 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.css'), gulp.series('copy')); 35 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.js'), gulp.series('copy')); 36 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.jsx'), gulp.series('transpile')); 37 | }); 38 | 39 | gulp.task('default', gulp.parallel('build', 'watch')); 40 | -------------------------------------------------------------------------------- /05.A Angular controllerAS & directive/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 05.A Angular integration 6 | 7 | 8 | 9 | 10 | 11 |
12 |

controllerAS syntax + react-component

13 |
14 |
15 |
16 |
17 |

Base64 encoder

18 |
19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /05.A Angular controllerAS & directive/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "legacy-apps", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Lemoncode/integrate-react-legacy-apps.git", 6 | "author": "Lemoncode ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "gulp build", 10 | "build:watch": "gulp" 11 | }, 12 | "dependencies": { 13 | "angular": "1.6.1", 14 | "bootstrap": "^3.3.7", 15 | "jquery": "^3.1.1", 16 | "ngreact": "^0.3.0", 17 | "prop-types": "^15.6.0", 18 | "react": "^16.2.0", 19 | "react-dom": "^16.2.0" 20 | }, 21 | "devDependencies": { 22 | "babel-preset-react": "^6.22.0", 23 | "del": "^2.2.2", 24 | "gulp": "gulpjs/gulp#4.0", 25 | "gulp-babel": "^6.1.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /05.A Angular controllerAS & directive/src/app.module.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app', ['react']); 5 | })(window.angular); 6 | -------------------------------------------------------------------------------- /05.A Angular controllerAS & directive/src/components/ShowEncoded.jsx: -------------------------------------------------------------------------------- 1 | (function (angular, React) { 2 | 'use strict'; 3 | 4 | var ShowEncoded = function (props) { 5 | return ( 6 |
7 |

8 | Encoded text 9 |

10 |
{props.encoded || 'Nothing written'}
11 |
12 | ); 13 | }; 14 | 15 | ShowEncoded.propTypes = { 16 | encoded: PropTypes.string.isRequired 17 | }; 18 | 19 | angular.module('app').value('ShowEncoded', ShowEncoded); 20 | })(window.angular, window.React); 21 | -------------------------------------------------------------------------------- /05.A Angular controllerAS & directive/src/controllers/main.controller.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | function Main() { 5 | var self = this; 6 | self.text = ''; 7 | self.encodedText = ''; 8 | self.encode = function (event) { 9 | event.preventDefault(); 10 | self.encodedText = btoa(self.text); 11 | self.text = ''; 12 | }; 13 | } 14 | angular.module('app').controller('Main', [Main]); 15 | })(window.angular); 16 | -------------------------------------------------------------------------------- /05.A Angular controllerAS & directive/src/css/styles.css: -------------------------------------------------------------------------------- 1 | .container, .container-fluid { 2 | margin-top: 50px; 3 | overflow: hidden; 4 | } 5 | -------------------------------------------------------------------------------- /05.B Angular controllerAS & factory/README.md: -------------------------------------------------------------------------------- 1 | # Angular 1.X integration: controllerAs + reactDirective 2 | 3 | ## Description 4 | 5 | This sample is one of the four _AngularJS integration_ samples that shows how to integrate React components in an existing AngularJS application. 6 | This particular sample is an alternative to the previous sample that uses a simple form created handled by a controller using _controllerAs_ syntax and `reactDirective` factory. 7 | 8 | ## Boilerplate 9 | 10 | We'll take as starting point sample [05.A Angular controllerAs & directive](../05.A\ Angular\ controllerAs\ &\ directive). 11 | 12 | ## Applying `reactDirective` 13 | 14 | We've seen we can create React elements wrapped in a `` directive. Now we'll use `reactDirective` factory to create as many directives as component definitions we have to use them directly instead of `` tag. 15 | 16 | Inside our `ShowEncoded.jsx` component we'll replace the export via `value` and create a directive `showEncoded` that accept the `reactDirective` factory as dependency and apply it to our component: 17 | 18 | ```diff 19 | (function (angular, React) { 20 | 'use strict'; 21 | 22 | + // React definition 23 | var ShowEncoded = function (props) { 24 | return ( 25 |
26 |

27 | Encoded text 28 |

29 |
{props.encoded || 'Nothing written'}
30 |
31 | ); 32 | }; 33 | 34 | ShowEncoded.propTypes = { 35 | encoded: PropTypes.string.isRequired 36 | }; 37 | 38 | + // AngularJS directive definition 39 | + var showEncoded = function (reactDirective) { 40 | + return reactDirective(ShowEncoded); 41 | + }; 42 | 43 | - angular.module('app').value('ShowEncoded', ShowEncoded); 44 | + angular.module('app').directive('showEncoded', ['reactDirective', showEncoded]); 45 | })(window.angular, window.React); 46 | ``` 47 | 48 | Finally let's change our implementation in `index.html`: 49 | 50 | ```diff 51 |
52 | - 56 | + 57 |
58 | ``` 59 | 60 | We've implemented our first component using `controlerAs` syntax and `reactDirectove` factory. In the next sample we'll see how to implement the same sample using component oriented and `react-component` directive. 61 | -------------------------------------------------------------------------------- /05.B Angular controllerAS & factory/gulpfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var gulp = require('gulp'); 3 | var babel = require('gulp-babel'); 4 | var del = require('del'); 5 | 6 | var BUILD_DIR = path.resolve(__dirname, 'src'); 7 | var DIST_DIR = path.resolve(__dirname, 'dist'); 8 | 9 | gulp.task('transpile', function () { 10 | return gulp 11 | .src(path.resolve(BUILD_DIR, '**', '*.jsx')) 12 | .pipe(babel({ 13 | presets: ['react'] 14 | })) 15 | .pipe(gulp.dest(DIST_DIR)); 16 | }); 17 | 18 | gulp.task('copy', function () { 19 | return gulp 20 | .src([ 21 | path.resolve(BUILD_DIR, '**', '*.css'), 22 | path.resolve(BUILD_DIR, '**', '*.js') 23 | ]) 24 | .pipe(gulp.dest(DIST_DIR)) 25 | }); 26 | 27 | gulp.task('clean', function () { 28 | return del(DIST_DIR); 29 | }); 30 | 31 | gulp.task('build', gulp.series('clean', gulp.parallel('copy', 'transpile'))); 32 | 33 | gulp.task('watch', function () { 34 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.css'), gulp.series('copy')); 35 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.js'), gulp.series('copy')); 36 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.jsx'), gulp.series('transpile')); 37 | }); 38 | 39 | gulp.task('default', gulp.parallel('build', 'watch')); 40 | -------------------------------------------------------------------------------- /05.B Angular controllerAS & factory/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 05.A Angular integration 6 | 7 | 8 | 9 | 10 | 11 |
12 |

controllerAS syntax + reactDirective

13 |
14 |
15 |
16 |
17 |

Base64 encoder

18 |
19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /05.B Angular controllerAS & factory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "legacy-apps", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Lemoncode/integrate-react-legacy-apps.git", 6 | "author": "Lemoncode ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "gulp build", 10 | "build:watch": "gulp" 11 | }, 12 | "dependencies": { 13 | "angular": "1.6.1", 14 | "bootstrap": "^3.3.7", 15 | "jquery": "^3.1.1", 16 | "ngreact": "^0.3.0", 17 | "prop-types": "^15.6.0", 18 | "react": "^16.2.0", 19 | "react-dom": "^16.2.0" 20 | }, 21 | "devDependencies": { 22 | "babel-preset-react": "^6.22.0", 23 | "del": "^2.2.2", 24 | "gulp": "gulpjs/gulp#4.0", 25 | "gulp-babel": "^6.1.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /05.B Angular controllerAS & factory/src/app.module.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app', ['react']); 5 | })(window.angular); 6 | -------------------------------------------------------------------------------- /05.B Angular controllerAS & factory/src/components/ShowEncoded.jsx: -------------------------------------------------------------------------------- 1 | (function (angular, React) { 2 | 'use strict'; 3 | 4 | // React component definition 5 | var ShowEncoded = function (props) { 6 | return ( 7 |
8 |

9 | Encoded text 10 |

11 |
{props.encoded || 'Nothing written'}
12 |
13 | ); 14 | }; 15 | 16 | ShowEncoded.propTypes = { 17 | encoded: PropTypes.string.isRequired 18 | }; 19 | 20 | // AngularJS directive definition 21 | var showEncoded = function (reactDirective) { 22 | return reactDirective(ShowEncoded); 23 | }; 24 | 25 | angular.module('app').directive('showEncoded', ['reactDirective', showEncoded]); 26 | })(window.angular, window.React); 27 | -------------------------------------------------------------------------------- /05.B Angular controllerAS & factory/src/controllers/main.controller.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | function Main() { 5 | var self = this; 6 | self.text = ''; 7 | self.encodedText = ''; 8 | self.encode = function (event) { 9 | event.preventDefault(); 10 | self.encodedText = btoa(self.text); 11 | self.text = ''; 12 | }; 13 | } 14 | angular.module('app').controller('Main', [Main]); 15 | })(window.angular); 16 | -------------------------------------------------------------------------------- /05.B Angular controllerAS & factory/src/css/styles.css: -------------------------------------------------------------------------------- 1 | .container, .container-fluid { 2 | margin-top: 50px; 3 | overflow: hidden; 4 | } 5 | -------------------------------------------------------------------------------- /05.C Angular components & directive/README.md: -------------------------------------------------------------------------------- 1 | # Angular 1.X integration: controllerAs + reactDirective 2 | 3 | ## Description 4 | 5 | This sample is one of the four _AngularJS integration_ samples that shows how to integrate React components in an existing AngularJS application. 6 | This particular sample uses a component base application that shows an accordion panel using `angular.component` and gets remplaced by a React accordion. 7 | 8 | ## Boilerplate 9 | 10 | We'll take as starting point sample [05.B Angular controllerAs & factory](../05.B\ Angular\ controllerAs\ &\ factory). 11 | 12 | ## Including web server 13 | 14 | We'll make few changes in `gulpfile.js` to include a web server because AngularJS `templateUrl` uses XMLHttpRequests to include templating and `file://` protocol is not included. 15 | 16 | - First install `gulp-connect` through `npm`. This will allow us to raise a lite web server up: 17 | 18 | ```shell 19 | npm install --save-dev gulp-connect 20 | ``` 21 | 22 | - Next let's add a Gulp task to configure the server: 23 | 24 | ```diff 25 | var path = require('path'); 26 | var gulp = require('gulp'); 27 | var babel = require('gulp-babel'); 28 | var del = require('del'); 29 | + var connect = require('gulp-connect'); 30 | 31 | var BUILD_DIR = path.resolve(__dirname, 'src'); 32 | var DIST_DIR = path.resolve(__dirname, 'dist'); 33 | 34 | + gulp.task('connect', function (done) { 35 | + connect.server({ 36 | + root: __dirname 37 | + }); 38 | + done(); 39 | + }); 40 | 41 | gulp.task('transpile', function () { 42 | 43 | ... 44 | 45 | gulp.task('copy', function () { 46 | return gulp 47 | .src([ 48 | path.resolve(BUILD_DIR, '**', '*.css'), 49 | + path.resolve(BUILD_DIR, '**', '*.html'), 50 | path.resolve(BUILD_DIR, '**', '*.js') 51 | ]) 52 | .pipe(gulp.dest(DIST_DIR)) 53 | }); 54 | 55 | ... 56 | 57 | gulp.task('watch', function (done) { 58 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.css'), gulp.series('copy')); 59 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.js'), gulp.series('copy')); 60 | + gulp.watch(path.resolve(BUILD_DIR, '**', '*.html'), gulp.series('copy')); 61 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.jsx'), gulp.series('transpile')); 62 | done(); 63 | }); 64 | 65 | ... 66 | 67 | - gulp.task('default', gulp.parallel('build', 'watch')); 68 | + gulp.task('default', gulp.parallel(gulp.series('build', 'connect'), 'watch')); 69 | ``` 70 | 71 | - Finally let's include a start script that boot the server in `package.json`: 72 | 73 | ```diff 74 | "license": "MIT", 75 | "scripts": { 76 | - "build": "gulp build", 77 | - "build:watch": "gulp" 78 | + "start": "gulp", 79 | + "build": "gulp build" 80 | }, 81 | "dependencies": { 82 | ``` 83 | 84 | ## Preparing the Angular accordion 85 | 86 | We'll remove `controllers` folder along with `ShowEncoded.jsx` file and create an `page-content`, `accordion` and `accordion-panel` with their templates in `components`. Our `src` folder structure should look like this: 87 | 88 | ``` 89 | src 90 | ├── app.module.js 91 | ├── components 92 | │   ├── accordion 93 | │   │   ├── accordion.html 94 | │   │   ├── accordion.js 95 | │   │   └── accordion-panel 96 | │   │   ├── accordion-panel.html 97 | │   │   └── accordion-panel.js 98 | │   ├── page-content 99 | │   │   ├── page-content.html 100 | │   │   └── page-content.js 101 | │   └── ShowEncoded.jsx 102 | └── css 103 | └── styles.css 104 | ``` 105 | 106 | The `page-content` component will be used as a container and layout former that will have a list of items to feed the accordion. 107 | 108 | ```html 109 | 110 |
111 |

Component based App

112 |
113 |
114 |
115 | 116 |
117 |
118 | ``` 119 | ```javascript 120 | // components/page-content/page-content.js 121 | (function (angular) { 122 | 'use strict'; 123 | 124 | function PageContent() { 125 | var self = this; 126 | 127 | self.feeds = [ 128 | { 129 | id: 32, 130 | heading: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Vitae, odio!', 131 | content: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Obcaecati vel, officia saepe cumque culpa alias quisquam rem repudiandae omnis dolorum doloremque, dicta pariatur unde iusto ex, eos neque laboriosam voluptatum.' 132 | }, 133 | { 134 | id: 33, 135 | heading: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ratione, ad!', 136 | content: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quisquam eum et ea harum laborum temporibus ab voluptates sunt, maxime dolore quas consequuntur vitae quos expedita nostrum quidem, minus, rem sit.' 137 | }, 138 | { 139 | id: 34, 140 | heading: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci, atque.', 141 | content: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eveniet, tempore aut consequuntur autem, repellat iste doloremque quibusdam sunt quos! At minus dicta debitis doloremque dolorem unde, maxime facilis voluptatum quam!' 142 | }, 143 | { 144 | id: 35, 145 | heading: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ex, asperiores!', 146 | content: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugiat, omnis dolores tempora officia consequuntur ratione sequi aliquid porro aut quisquam quas obcaecati facere assumenda minima odit reiciendis laboriosam natus! Eum!' 147 | }, 148 | ]; 149 | } 150 | 151 | angular 152 | .module('app') 153 | .component('pageContent', { 154 | controller: PageContent, 155 | templateUrl: './dist/components/page-content/page-content.html' 156 | }); 157 | })(angular); 158 | ``` 159 | 160 | `PageContent` component will be implemented directly inside `` in `index.html`: 161 | 162 | ```html 163 | ... 164 | 165 | 166 | 167 | ... 168 | ``` 169 | 170 | The `accordion` component will be a simple wrapper that accepts a `feeds` array and render as many panes as items in `feeds`: 171 | 172 | ```html 173 | 174 |
175 | 176 |
177 | ``` 178 | 179 | ```javascript 180 | // components/accordion/accordion.js 181 | (function (angular) { 182 | 'use strict'; 183 | 184 | function AccordionController() { 185 | var self = this; 186 | var panels = []; 187 | 188 | self.addPanel = function (panel) { 189 | panels.push(panel); 190 | if (panels.length) { 191 | panels[0].show(); 192 | } 193 | }; 194 | 195 | self.select = function (selectedPanel) { 196 | panels.forEach(function (panel) { 197 | if (panel === selectedPanel) { 198 | panel.show(); 199 | } else { 200 | panel.hide(); 201 | } 202 | }); 203 | }; 204 | } 205 | 206 | angular 207 | .module('app') 208 | .component('accordion', { 209 | bindings: { 210 | feeds: '<' 211 | }, 212 | templateUrl: './dist/components/accordion/accordion.html', 213 | controller: AccordionController 214 | }); 215 | })(window.angular); 216 | ``` 217 | 218 | The `accordion-panel` will show the heading and content of the feed. It will receive the `feed` object and render the panel. 219 | 220 | ```html 221 | 222 |
223 |
224 |

{{$ctrl.feed.heading}}

225 |
226 |
227 |

{{$ctrl.feed.content}}

228 |
229 |
230 | ``` 231 | 232 | ```javascript 233 | // components/accordion-panel/accordion-panel.js 234 | (function (angular) { 235 | 'use strict'; 236 | 237 | function AccordionPanel() { 238 | var self = this; 239 | var selected = false; 240 | 241 | self.$onInit = function () { 242 | self.parent.addPanel(self); 243 | }; 244 | 245 | self.select = function () { 246 | self.parent.select(self); 247 | }; 248 | 249 | self.show = function () { 250 | if (selected) { 251 | self.hide(); 252 | } else { 253 | selected = true; 254 | self.active = 'in'; 255 | } 256 | }; 257 | 258 | self.hide = function () { 259 | selected = false; 260 | self.active = ''; 261 | }; 262 | } 263 | 264 | angular 265 | .module('app') 266 | .component('accordionPanel', { 267 | bindings: { 268 | feed: '<' 269 | }, 270 | require: { 271 | parent: '^accordion' 272 | }, 273 | templateUrl: './dist/components/accordion/accordion-panel/accordion-panel.html', 274 | controller: AccordionPanel 275 | }); 276 | })(window.angular); 277 | ``` 278 | 279 | Finally let's add some styles in `styles.css` file to animate the accordion: 280 | 281 | ```diff 282 | .container, .container-fluid { 283 | margin-top: 50px; 284 | overflow: hidden; 285 | } 286 | 287 | + .panel-body.collapsible { 288 | + transition: all .30s ease-out; 289 | + max-height: 0; 290 | + overflow: hidden; 291 | + padding-top: 0; 292 | + padding-bottom: 0; 293 | + } 294 | + 295 | + .panel-body.collapsible.in { 296 | + max-height: 100px; 297 | + transition: all .30s ease-out; 298 | + padding-top: 15px; 299 | + padding-bottom: 15px; 300 | + } 301 | + 302 | + .pointer { 303 | + cursor: pointer; 304 | + } 305 | + 306 | + .feed-content{ 307 | + margin: 0; 308 | + } 309 | ``` 310 | 311 | Let's add their script references in `index.html`: 312 | 313 | ```html 314 | ... 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | ... 326 | ``` 327 | 328 | If we open a command line prompt, locate to the root directory of the project and execute `npm start` we'll see at [localhost:8080](http://localhost:8080) the next result: 329 | 330 | ![01boilerplate.gif](../99_readme_resources/01boilerplate.gif) 331 | 332 | ## Replacing the Accordion UI Component 333 | 334 | ### Replacing the `AccordionPanel` 335 | 336 | Let's replace the accordion with a React one. First we'll replace the lowest component, the `AccordionPanel`, rename it to `accordion-panel.jsx` and change its implementation: 337 | 338 | ```diff 339 | + (function (React, angular) { 340 | - (function (angular) { 341 | 'use strict'; 342 | 343 | + function AccordionPanel(props) { 344 | + var select = function () { 345 | + return props.onSelect(props.feed.id); 346 | + }; 347 | + var className = 'panel - body collapsible'; 348 | + if (props.active) { 349 | + className += ' in'; 350 | + } 351 | + return ( 352 | +
353 | +
354 | +

{props.feed.heading}

355 | +
356 | +
357 | +

{props.feed.content}

358 | +
359 | +
360 | + ); 361 | + } 362 | + 363 | + AccordionPanel.displayName = 'AccordionPanel'; 364 | + AccordionPanel.propTypes = { 365 | + active: PropTypes.bool, 366 | + onSelect: PropTypes.func, 367 | + feed: PropTypes.shape({ 368 | + id: PropTypes.number.isRequired, 369 | + heading: PropTypes.string, 370 | + content: PropTypes.string 371 | + }) 372 | + }; 373 | - function AccordionPanel() { 374 | - var self = this; 375 | - var selected = false; 376 | - self.$onInit = function () { 377 | - self.parent.addPanel(self); 378 | - }; 379 | - self.select = function () { 380 | - self.parent.select(self); 381 | - }; 382 | - self.show = function () { 383 | - if (selected) { 384 | - self.hide(); 385 | - } else { 386 | - selected = true; 387 | - self.active = 'in'; 388 | - } 389 | - }; 390 | - self.hide = function () { 391 | - selected = false; 392 | - self.active = ''; 393 | - }; 394 | - } 395 | 396 | angular 397 | .module('app') 398 | + .value('AccordionPanel', AccordionPanel); 399 | - .component('accordionPanel', { 400 | - bindings: { 401 | - feed: '<' 402 | - }, 403 | - require: { 404 | - parent: '^accordion' 405 | - }, 406 | - templateUrl: ./dist/components/accordion/accordion-panel/accordion-pane.html', 407 | - controller: AccordionPanel 408 | - }); 409 | + })(window.React, window.angular); 410 | - })(window.angular); 411 | ``` 412 | This _dumb_ component will basically expect some properties to work: 413 | 414 | - `active` property, if `true` `AccordionPanel` will apply class `in` to show the content. 415 | - `onSelect` property, a callback to notify `Accordion` that one was selected by the feed `id` (_dumb_ components should only handle render logic instead of state logic). 416 | - `feed` property, the feed that will be shown with the heading and content. 417 | 418 | We register the component inside the `app` module so as not to make it global or store it in another namespace. 419 | 420 | ### Replacing the `Accordion` 421 | 422 | Now it's time to replace the `Accordion` component by an _container_ component that stores some logic and gets the feeds by an angular component. Start by renaming it to `accordion.jsx` and then change its implementation: 423 | 424 | ```diff 425 | + (function (React, angular) { 426 | - (function (angular) { 427 | 'use strict'; 428 | 429 | + var Accordion = function (AccordionPanel) { 430 | + return class Accordion extends React.Component { 431 | + 432 | + constructor(props) { 433 | + super(props); 434 | + this.state = { selected: null }; 435 | + this.select = this.select.bind(this); 436 | + } 437 | + 438 | + select(selected) { 439 | + if (selected === this.state.selected) { 440 | + selected = null; 441 | + } 442 | + this.setState({ selected }); 443 | + } 444 | + 445 | + render() { 446 | + var feeds = this.props.feeds || ['a', 'b', 'c', 'd']; 447 | + var selected = this.state.selected; 448 | + var self = this; 449 | + return ( 450 | +
451 | +

{this.props.foo}

452 | + {feeds.map(function (feed, index) { 453 | + return ( 454 | + 461 | + ); 462 | + })} 463 | +
464 | + ); 465 | + } 466 | + } 467 | + } 468 | - function AccordionController() { 469 | - var self = this; 470 | - var panels = []; 471 | - 472 | - self.addPanel = function (panel) { 473 | - panels.push(panel); 474 | - if (panels.length) { 475 | - panels[0].show(); 476 | - } 477 | - }; 478 | - 479 | - self.select = function (selectedPanel) { 480 | - panels.forEach(function (panel) { 481 | - if (panel === selectedPanel) { 482 | - panel.show(); 483 | - } else { 484 | - panel.hide(); 485 | - } 486 | - }); 487 | - }; 488 | - } 489 | - 490 | angular 491 | .module('app') 492 | + .factory('Accordion', ['AccordionPanel', Accordion]); 493 | - .component('accordion', { 494 | - bindings: { 495 | - feeds: '<' 496 | - }, 497 | - templateUrl: './dist/components/accordion/accordion.html', 498 | - controller: AccordionController 499 | - }); 500 | - })(window.angular); 501 | + })(window.React, window.angular); 502 | ``` 503 | As the AngularJS `Accordion` component it will receive a list of `feeds` and pass each feed to a child. This `Accordion` component will handle the activation of each `AccordionPanel` and store the selected one given the feed `id`. 504 | 505 | > Note: Since we stored `AccordionPanel` inside AngularJS's environment to use it we need to create a `angular.factory` that returns the React component and receives the definition of `AccordionPanel` to use. Also, the `AccordionPanel` definition must be PascalCase to be used dynamically and not to be interpreted as a DOM element such as `'div'`, `'span'`, etc, when generating `React.createElement` through transpilation. 506 | 507 | Finally we'll make a change in `page-content.html` to call the new React `Accordion` component: 508 | 509 | ```diff 510 |
511 |

Component based App

512 |
513 |
514 |
515 | - 516 | + 517 |
518 |
519 | ``` 520 | 521 | > Note: Since we're using a factory as a component instead of a AngularJS directive it's not possible use `reactDirective` factory to implement `Accordion` _tag like_. 522 | 523 | Now accordion component has been migrated to React components (we still have them inside AngularJS though) there are some files we are not using. Let's remove `accordion-panel.html` and `accordion.html` and place `accordion-panel.jsx` inside `accordion` folder: 524 | 525 | ``` 526 | src 527 | ├── app.module.js 528 | ├── components 529 | │   ├── accordion 530 | │   │   ├── accordion.jsx 531 | │   │   └── accordion-panel.jsx 532 | │   └── page-content 533 | │   ├── page-content.html 534 | │   └── page-content.js 535 | ├── css 536 | │   └── styles.css 537 | └── service.js 538 | ``` 539 | 540 | Next let's change its references inside `index.html`: 541 | 542 | ```diff 543 | ... 544 | 545 | - 546 | + 547 | 548 | 549 | 550 | ``` 551 | 552 | Finally open a command prompt, locate yourself on the root folder of the project and execute `npm start`. You can see the result in [localhost:8080](http://localhost:8080). 553 | -------------------------------------------------------------------------------- /05.C Angular components & directive/gulpfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var gulp = require('gulp'); 3 | var babel = require('gulp-babel'); 4 | var del = require('del'); 5 | var connect = require('gulp-connect'); 6 | 7 | var BUILD_DIR = path.resolve(__dirname, 'src'); 8 | var DIST_DIR = path.resolve(__dirname, 'dist'); 9 | 10 | gulp.task('connect', function (done) { 11 | connect.server({ 12 | root: __dirname 13 | }); 14 | done(); 15 | }); 16 | 17 | 18 | gulp.task('transpile', function () { 19 | return gulp 20 | .src(path.resolve(BUILD_DIR, '**', '*.jsx')) 21 | .pipe(babel({ 22 | presets: ['react'] 23 | })) 24 | .pipe(gulp.dest(DIST_DIR)); 25 | }); 26 | 27 | gulp.task('copy', function () { 28 | return gulp 29 | .src([ 30 | path.resolve(BUILD_DIR, '**', '*.css'), 31 | path.resolve(BUILD_DIR, '**', '*.html'), 32 | path.resolve(BUILD_DIR, '**', '*.js') 33 | ]) 34 | .pipe(gulp.dest(DIST_DIR)) 35 | }); 36 | 37 | gulp.task('clean', function () { 38 | return del(DIST_DIR); 39 | }); 40 | 41 | gulp.task('build', gulp.series('clean', gulp.parallel('copy', 'transpile'))); 42 | gulp.task('watch', function (done) { 43 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.css'), gulp.series('copy')); 44 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.js'), gulp.series('copy')); 45 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.html'), gulp.series('copy')); 46 | gulp.watch(path.resolve(BUILD_DIR, '**', '*.jsx'), gulp.series('transpile')); 47 | done(); 48 | }); 49 | 50 | gulp.task('dev', gulp.parallel('build', 'watch')); 51 | gulp.task('default', gulp.parallel(gulp.series('build', 'connect'), 'watch')); 52 | -------------------------------------------------------------------------------- /05.C Angular components & directive/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 05.A Angular integration 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /05.C Angular components & directive/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "legacy-apps", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/Lemoncode/integrate-react-legacy-apps.git", 6 | "author": "Lemoncode ", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "gulp", 10 | "build": "gulp build" 11 | }, 12 | "dependencies": { 13 | "angular": "1.6.1", 14 | "bootstrap": "^3.3.7", 15 | "jquery": "^3.1.1", 16 | "ngreact": "^0.3.0", 17 | "prop-types": "^15.6.0", 18 | "react": "^16.2.0", 19 | "react-dom": "^16.2.0" 20 | }, 21 | "devDependencies": { 22 | "babel-preset-react": "^6.22.0", 23 | "del": "^2.2.2", 24 | "gulp": "gulpjs/gulp#4.0", 25 | "gulp-babel": "^6.1.2", 26 | "gulp-connect": "^5.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /05.C Angular components & directive/src/app.module.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | angular.module('app', ['react']); 5 | })(window.angular); 6 | -------------------------------------------------------------------------------- /05.C Angular components & directive/src/components/accordion/accordion-panel.jsx: -------------------------------------------------------------------------------- 1 | (function (React, angular) { 2 | 'use strict'; 3 | 4 | function AccordionPanel(props) { 5 | var select = function () { 6 | return props.onSelect(props.feed.id); 7 | }; 8 | var className = 'panel-body collapsible'; 9 | if (props.active) { 10 | className += ' in'; 11 | } 12 | return ( 13 |
14 |
15 |

{props.feed.heading}

16 |
17 |
18 |

{props.feed.content}

19 |
20 |
21 | ); 22 | } 23 | 24 | AccordionPanel.displayName = 'AccordionPanel'; 25 | AccordionPanel.propTypes = { 26 | active: PropTypes.bool, 27 | onSelect: PropTypes.func, 28 | feed: PropTypes.shape({ 29 | id: PropTypes.number.isRequired, 30 | heading: PropTypes.string, 31 | content: PropTypes.string 32 | }) 33 | }; 34 | 35 | angular 36 | .module('app') 37 | .value('AccordionPanel', AccordionPanel); 38 | })(window.React, window.angular); 39 | -------------------------------------------------------------------------------- /05.C Angular components & directive/src/components/accordion/accordion.jsx: -------------------------------------------------------------------------------- 1 | (function (React, angular) { 2 | 'use strict'; 3 | 4 | var Accordion = function (AccordionPanel) { 5 | 6 | return class Accordion extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { selected: null }; 11 | this.select = this.select.bind(this); 12 | } 13 | 14 | select(selected) { 15 | if (selected === this.state.selected) { 16 | selected = null; 17 | } 18 | this.setState({ selected }); 19 | } 20 | 21 | render() { 22 | var feeds = this.props.feeds || ['a', 'b', 'c', 'd']; 23 | var selected = this.state.selected; 24 | var self = this; 25 | return ( 26 |
27 |

{this.props.foo}

28 | {feeds.map(function (feed, index) { 29 | return ( 30 | 37 | ); 38 | })} 39 |
40 | ); 41 | } 42 | } 43 | } 44 | 45 | angular 46 | .module('app') 47 | .factory('Accordion', ['AccordionPanel', Accordion]); 48 | })(window.React, window.angular); 49 | 50 | -------------------------------------------------------------------------------- /05.C Angular components & directive/src/components/page-content/page-content.html: -------------------------------------------------------------------------------- 1 |
2 |

Component based App

3 |
4 |
5 |
6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /05.C Angular components & directive/src/components/page-content/page-content.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 'use strict'; 3 | 4 | function PageContent() { 5 | var self = this; 6 | 7 | self.feeds = [ 8 | { 9 | id: 32, 10 | heading: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Vitae, odio!', 11 | content: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Obcaecati vel, officia saepe cumque culpa alias quisquam rem repudiandae omnis dolorum doloremque, dicta pariatur unde iusto ex, eos neque laboriosam voluptatum.' 12 | }, 13 | { 14 | id: 33, 15 | heading: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ratione, ad!', 16 | content: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quisquam eum et ea harum laborum temporibus ab voluptates sunt, maxime dolore quas consequuntur vitae quos expedita nostrum quidem, minus, rem sit.' 17 | }, 18 | { 19 | id: 34, 20 | heading: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci, atque.', 21 | content: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eveniet, tempore aut consequuntur autem, repellat iste doloremque quibusdam sunt quos! At minus dicta debitis doloremque dolorem unde, maxime facilis voluptatum quam!' 22 | }, 23 | { 24 | id: 35, 25 | heading: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ex, asperiores!', 26 | content: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugiat, omnis dolores tempora officia consequuntur ratione sequi aliquid porro aut quisquam quas obcaecati facere assumenda minima odit reiciendis laboriosam natus! Eum!' 27 | }, 28 | ]; 29 | } 30 | 31 | angular 32 | .module('app') 33 | .component('pageContent', { 34 | controller: PageContent, 35 | templateUrl: './dist/components/page-content/page-content.html' 36 | }); 37 | })(angular); 38 | -------------------------------------------------------------------------------- /05.C Angular components & directive/src/css/styles.css: -------------------------------------------------------------------------------- 1 | .container, .container-fluid { 2 | margin-top: 50px; 3 | overflow: hidden; 4 | } 5 | 6 | .panel-body.collapsible { 7 | transition: all .30s ease-out; 8 | max-height: 0; 9 | overflow: hidden; 10 | padding-top: 0; 11 | padding-bottom: 0; 12 | } 13 | 14 | 15 | .panel-body.collapsible.in { 16 | max-height: 100px; 17 | transition: all .30s ease-out; 18 | padding-top: 15px; 19 | padding-bottom: 15px; 20 | } 21 | 22 | .pointer { 23 | cursor: pointer; 24 | } 25 | 26 | .feed-content { 27 | margin-bottom: 0; 28 | } 29 | -------------------------------------------------------------------------------- /99_readme_resources/00boilerplate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/integrate-react-legacy-apps/b470c50611692f062f7882b58f6a7520134bc36a/99_readme_resources/00boilerplate.gif -------------------------------------------------------------------------------- /99_readme_resources/01boilerplate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/integrate-react-legacy-apps/b470c50611692f062f7882b58f6a7520134bc36a/99_readme_resources/01boilerplate.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Integrating Reat with legacy applications** 2 | 3 | # The so feared migration 4 | 5 | The time has arrived... After ten years developing on our beloved web technology (ASP.NET Web forms, PHP, ASP.NET MVC, Ruby...) someone from marketing department came with the following complaints about the web application: 6 | 7 | * Some customers want to be able to work from the sofa on their mobile devices but they can't, they need to have computer around. 8 | * Some customers cannot complete their orders. 9 | * 30% of our customers have cheap mobile phones and our website is too heavy for them. 10 | * When a customer is in the country-side he cannot work on our site because the connection is too slow for our application. 11 | 12 | That translates to our "language" as: 13 | 14 | * We have a poor web, not responsive and not adapted to mobile device interaction gestures. 15 | * We have so much logic in spaghetti JavaScript that it's impossible to manage, same reason why our application throws unexpected errors. 16 | * Our application is too dependant on server side tasks that could simply be done on the client side. 17 | * Our application is too heavy and requires too much bandwidth consumption, mobile battery and resources. 18 | 19 | The worst... this is happening with not so old technologies. Remember of Angular 1? Do you have performance issues? 20 | 21 | Now it's time to choose. Probably your app is a massive juggernaut and you cannot just close the business for a couple years and completely migrate it. 22 | 23 | Isn't there a way to migrate little by little?... **React to the rescue!** 24 | 25 | # React 26 | 27 | React is a light library for user interface rendering that has a [very good performance](https://evancz.github.io/react-angular-ember-elm-performance-comparison/), 28 | also it allows building pages out of components. It will make it possible to replace some parts of a view and **work together with older libraries**. Let's see how: 29 | 30 | ### Approximations 31 | 32 | One can use the following approximations for progressively migrate a legacy application with React. 33 | 34 | * [Option 1 - Presentational React components with jQuery](#option-1) 35 | * [Option 2 - Stateful React components with jQuery](#option-2) 36 | * [Option 3 - Pub/Sub pattern through jQuery $.Callbacks](#option-3) 37 | * [Option 4 - React inside MVC based Angular 1.x applications](#option-4) 38 | * [Option 5 - React inside components based Angular 1.x applications](#option-5) 39 | 40 | 41 | | All the examples below are public and available on this same repository [integrate-react-legacy-apps]() | 42 | | --------------------------------------------------------------------------------------------------------------------- | 43 | 44 | 45 | 46 | 47 | ### Option 1 - Presentational React components with jQuery 48 | 49 | Even though React and jQuery are two very different libraries used for solving different problems in different ways (jQuery is based on direct DOM manipulation while React aims to avoiding DOM manipulation as much as possible) they are both capable to coexist. 50 | 51 | A basic example would be having a module that requests server data right on initialization and pushes this data into a table rendered to the user. 52 | 53 | ![Image option 1.1](https://static1.squarespace.com/static/56cdb491a3360cdd18de5e16/t/58b6fda1bebafb0976fe11ea/1488387495490/?format=750w) 54 | 55 | We can create a small function extending jQuery prototype and allowing, for a given selector, to load a React component over that same element. 56 | 57 | ```javascript 58 | $.fn.extend({ 59 | react: function (Component, props, callback) { 60 | var mountedComponent = ReactDOM.render( 61 | , 62 | this.get(0) 63 | ); 64 | 65 | if (typeof callback === 'function') { 66 | return callback(mountedComponent); 67 | } 68 | 69 | return mountedComponent; 70 | } 71 | }); 72 | ``` 73 | 74 | From now, calling the ```react``` function over a jQuery selected element, will [load](append?replace?) the passed in React component in the DOM node of the element. 75 | 76 | For the current example we will mount the ```ContactsTableComponent``` React component with the following code: 77 | 78 | ```javascript 79 | var ContactsTableComponent = App.components.ContactsTableComponent; 80 | var contacts, $mountedTableComponent; 81 | 82 | // Initialize components 83 | var createReactComponents = function () { 84 | $mountedContactsTableComponent = $('#tableComponent'); 85 | showContacts(null); // First render with no data 86 | }; 87 | 88 | // Fill table, then mount/update React component 89 | var showContacts = function (contacts, callback) { 90 | $mountedContactsTableComponent.react(ContactsTableComponent, { contacts: contacts || [] }, callback); 91 | }; 92 | ``` 93 | 94 | This implementation uses the jQuery selectors as an entry for embedding React components. The model data is stored in the page and loaded to the React component through **properties**. 95 | 96 | And the React component definition: 97 | 98 | ```javascript 99 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 100 | var ContactRowComponent = App.components.ContactRowComponent; 101 | 102 | var ContactsTableComponent = function (props) { 103 | var contacts = props.contacts || []; 104 | return ( 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | {contacts.map(function (contact, index) { 115 | return ; 116 | })} 117 | 118 |
NamePhone numberEmail
119 | ); 120 | }; 121 | 122 | ContactsTableComponent.displayName = 'ContactsTableComponent'; 123 | ContactsTableComponent.propTypes = { 124 | contacts: React.PropTypes.arrayOf(ContactPropTypes) 125 | }; 126 | ``` 127 | 128 | This way, when responding to an AJAX request for the component data, it can get updated by calling the ```showContacts``` function with the response data: 129 | 130 | ```javascript 131 | var fetchContacts = function () { 132 | $.when(contactsService.fetchContacts()) 133 | .then(function (fetchedContacts) { 134 | showContacts(fetchedContacts); 135 | }); 136 | }; 137 | ``` 138 | 139 | ![Image Option 1.2](https://static1.squarespace.com/static/56cdb491a3360cdd18de5e16/t/58b700859f74569e0581b942/1488388248574/?format=750w) 140 | 141 | 142 | | This example source code is available at [02 Props and Render](https://github.com/Lemoncode/integrate-react-legacy-apps/tree/master/02%20Props%20and%20Render) | 143 | | :---------------------------------------------------------------------------------------------------------------------: | 144 | 145 | 146 | ### Option 2 - Stateful React components with jQuery 147 | 148 | When we work with components it is usual to have components with more presentation logic and need to store an internal state. These are commonly named **container components**. A container component is just a component in charge of managing the state of a part of the application, in other words, it's in charge of business logic. 149 | 150 | From jQuery we could then execute more actions apart from mounting, as accessing component properties and public methods, allowing component state changes. 151 | 152 | Then we would change ```showContacts``` function implementation as: 153 | 154 | ```javascript 155 | var ContactsTableContainer = App.components.ContactsTableContainer; 156 | var contacts, $mountedContactsTableContainer; 157 | 158 | var createReactComponents = function () { 159 | $mountedContactsTableContainer = $('#tableComponent').react(ContactsTableContainer, null); 160 | }; 161 | 162 | var showContacts = function (contacts, callback) { 163 | // Accessing React component API methods 164 | $mountedContactsTableContainer.setState({ contacts: contacts }); 165 | }; 166 | ``` 167 | 168 | In this case the component instance, mounted by ReactDOM is stored in a variable. This way we can call it's method setState to modify the state. 169 | It's important to mention that this call could be perfectly encapsulated in a public method implemented to do some validations right before changing the state. 170 | 171 | This option can be useful in some scenarios, but it's not the best case scenario, since calling some lifecycle methods [in a wrong way](this needs explanation) can have unexpected effects. 172 | 173 | It is more common that the React components are those who interact with each other through the methods inside their input properties. 174 | 175 | ![Image Option 2.1](https://static1.squarespace.com/static/56cdb491a3360cdd18de5e16/t/58b7019c29687f41be932a36/1488388521762/?format=750w) 176 | 177 | 178 | | The complete implementation can be found at [03 Stateful Component](https://github.com/Lemoncode/integrate-react-legacy-apps/tree/master/03%20Stateful%20Component) | 179 | | :---------------------------------------------------------------------------------------------------------------------: | 180 | 181 | ### Option 3 - Pub/Sub pattern through jQuery $.Callbacks 182 | 183 | Publish-Subscribe pattern can be very useful for communicating React with jQuery since the actions sent by the communication channels can be sent to those functions subscribed to the channel without knowing anything about the rest of he listeners. Let's see how we could add a simple implementation of the Pub/Sub pattern by a method that can be accessed via jQuery: 184 | 185 | ```javascript 186 | $.observe = (function () { 187 | var subjects = {}; 188 | return function (id) { 189 | var callbacks; 190 | var subject = id && subjects[id]; 191 | 192 | if (!subject) { 193 | callbacks = $.Callbacks(); 194 | subject = { 195 | publish: callbacks.fire, 196 | subscribe: callbacks.add, 197 | unsubscribe: callbacks.remove 198 | }; 199 | if (id) { 200 | subjects[id] = subject; 201 | } 202 | } 203 | return subject; 204 | }; 205 | })(); 206 | ``` 207 | 208 | The ```subject``` object stores the communication channel and exposes the *publish*, *subscribe* and *unsubscribe* actions. 209 | In this pattern implementation, React components subscribe and un-subscribe to the communication channel inside the ```componentDidMount``` and ```componentWillUnmount``` lifecycle methods. 210 | 211 | ```javascript 212 | var ContactPropTypes = App.PropTypes.ContactPropTypes; 213 | var ContactsTableComponent = App.components.ContactsTableComponent; 214 | 215 | var ContactsTableContainer = React.createClass({ 216 | displayName: 'ContactsTableContainer', 217 | onAddContact: function (contact) { 218 | this.setState({ 219 | contacts: this.state.contacts.concat(contact) 220 | }); 221 | }, 222 | componentDidMount: function () { 223 | $.observe('addContacts').subscribe(this.onAddContact); 224 | }, 225 | componentWillUnmount: function () { 226 | $.observe('addContacts').unsubscribe(this.onAddContact); 227 | }, 228 | getInitialState: function () { 229 | return { 230 | contacts: [] 231 | }; 232 | }, 233 | render: function () { 234 | return ; 235 | } 236 | }); 237 | ``` 238 | 239 | The component exposes a method, in this case the ```onAddContact``` method, that modifies the component state when $.observe('').publish method is called where is 'addContacts'. This way, when our application needs to change the component state, we just have to send the data through this channel. As an example: 240 | 241 | ```javascript 242 | var fetchContacts = function () { 243 | $.when(contactsService.fetchContacts()) 244 | .then(function (fetchedContacts) { 245 | contacts = fetchedContacts; 246 | $.observe('addContacts').publish(contacts); 247 | }); 248 | }; 249 | ``` 250 | 251 | And when publishing to 'addContacts' channel, the contacts are received through the ```onAddContact``` method, changing its state and activating its lifecycle events like ```render``` resulting in an HTML change. 252 | 253 | By using this pattern, it is not necessary to store component instances in the module. 254 | 255 | ```javascript 256 | var ContactsTableContainer = App.components.ContactsTableContainer; 257 | var contacts; 258 | 259 | var createReactComponents = function () { 260 | ReactDOM.render( 261 | , 262 | $('#tableComponent').get(0) 263 | ); 264 | }; 265 | ``` 266 | 267 | ![Image option 3.1](https://static1.squarespace.com/static/56cdb491a3360cdd18de5e16/t/58b6fcc21b631b3f18ac39cd/1488387283256/?format=750w) 268 | 269 | 270 | | The complete implementation can be found at [ 04 Event Emitters](https://github.com/Lemoncode/integrate-react-legacy-apps/tree/master/04%20Event%20Emitters) | 271 | | :---------------------------------------------------------------------------------------------------------------------: | 272 | 273 | 274 | ### Option 4 - React inside Angular 1.x with MVC architecture 275 | 276 | AngularJS allows components creation as a directive and it makes it easier to integrate React components thanks to the ngReact library that provides the ```react-component``` directive and the ```reactDirective``` factory. 277 | 278 | We will demonstrate with a small example where we have a small form requesting data to be codified in base64. We will integrate a React component to show its result by using ```react-component```: 279 | 280 | ##### Form (index.html) 281 | 282 | ```html 283 |
284 |
285 |

Base64 encoder

286 |
287 | 288 | 289 |
290 |
291 | 292 |
293 |
294 |
295 | 296 |
297 |
298 | ``` 299 | 300 | ##### App module 301 | 302 | We create the module with a dependency of ngReact. 303 | ``` 304 | angular.module('app', ['react']); 305 | ``` 306 | 307 | ##### Controller 308 | 309 | And the controller to store the model. 310 | ```javascript 311 | // src/controllers/main-controller.js 312 | function Main() { 313 | var self = this; 314 | self.text = ''; 315 | self.encodedText = ''; 316 | self.encode = function (event) { 317 | event.preventDefault(); 318 | self.encodedText = btoa(self.text); 319 | self.text = ''; 320 | }; 321 | } 322 | angular.module('app').controller('Main', [Main]); 323 | ``` 324 | 325 | ##### React component 326 | 327 | And finally the React component that will receive the codified text through ```props``` properties. 328 | 329 | ```javascript 330 | var ShowEncoded = function (props) { 331 | return ( 332 |
333 |

Encoded text

334 |
{props.encoded || 'Nothing written'}
335 |
336 | ); 337 | }; 338 | 339 | ShowEncoded.propTypes = { 340 | encoded: React.PropTypes.string.isRequired 341 | }; 342 | 343 | // Store React component in Angular module 344 | angular.module('app').value('ShowEncoded', ShowEncoded); 345 | ``` 346 | 347 | ##### Calling the component 348 | To call the component we just have to add the react-component tag in the form as: 349 | 350 | ```html 351 | 352 | ``` 353 | 354 | Internally ngReact delegates the model properties to the component through ```props``` attribute when they change, making it to be updated. Quite simple, right? 355 | 356 | 357 | | The complete implementation can be found at [ 05.A Angular controllerAS & Directive.](https://github.com/Lemoncode/integrate-react-legacy-apps/tree/master/05.A%20Angular%20controllerAS%20%26%20directive) | 358 | | :---------------------------------------------------------------------------------------------------------------------: | 359 | 360 | Another way to add a component to our Angular application is changing the component export using ngReact ```reactDirective``` factory. 361 | 362 | ```javascript 363 | // AngularJS directive definition 364 | var showEncoded = function (reactDirective) { 365 | return reactDirective(ShowEncoded); 366 | }; 367 | 368 | angular.module('app').directive('showEncoded', ['reactDirective', showEncoded]); 369 | ``` 370 | 371 | This way our component can be added to the form as: 372 | 373 | ```html 374 | 375 | ``` 376 | 377 | ![Image option 4.1](https://static1.squarespace.com/static/56cdb491a3360cdd18de5e16/t/58b6feaff5e23157a3f8c5af/1488387768685/?format=750w) 378 | 379 | | The complete implementation can be found at [ 05.B Angular controllerAS & factory.](https://github.com/Lemoncode/integrate-react-legacy-apps/tree/master/05.B%20Angular%20controllerAS%20%26%20factory) | 380 | | :---------------------------------------------------------------------------------------------------------------------: | 381 | 382 | 383 | ### Option 5 - React inside components based Angular 1.x applications 384 | 385 | From Angular 1.5 it is possible to use angular.component to make our app out of components. The application can be based on components that work as small reusable pieces. 386 | We'll picture this with an example of an acordion component built in Angular that we want to migrate to React. 387 | 388 | ### Acordion Controller 389 | 390 | ```javascript 391 | function AccordionController() { 392 | var self = this; 393 | var panels = []; 394 | 395 | self.addPanel = function (panel) { 396 | panels.push(panel); 397 | if (panels.length) { 398 | panels[0].show(); 399 | } 400 | }; 401 | 402 | self.select = function (selectedPanel) { 403 | panels.forEach(function (panel) { 404 | if (panel === selectedPanel) { 405 | panel.show(); 406 | } else { 407 | panel.hide(); 408 | } 409 | }); 410 | }; 411 | } 412 | 413 | angular 414 | .module('app') 415 | .component('accordion', { 416 | bindings: { 417 | feeds: '<' 418 | }, 419 | templateUrl: './dist/components/accordion/accordion.html', 420 | controller: AccordionController 421 | }); 422 | ``` 423 | 424 | ##### Accordion Template 425 | 426 | ```html 427 | 428 |
429 | 430 |
431 | ``` 432 | 433 | This accordion will render a set of panels containing the received feeds from a parent component. The panel is just another reusable component with a controller and a template defined as: 434 | 435 | ```javascript 436 | function AccordionPanel() { 437 | var self = this; 438 | var selected = false; 439 | 440 | self.$onInit = function () { 441 | self.parent.addPanel(self); 442 | }; 443 | 444 | self.select = function () { 445 | self.parent.select(self); 446 | }; 447 | 448 | self.show = function () { 449 | if (selected) { 450 | self.hide(); 451 | } else { 452 | selected = true; 453 | self.active = 'in'; 454 | } 455 | }; 456 | 457 | self.hide = function () { 458 | selected = false; 459 | self.active = ''; 460 | }; 461 | } 462 | 463 | angular 464 | .module('app') 465 | .component('accordionPanel', { 466 | bindings: { 467 | feed: '<' 468 | }, 469 | require: { 470 | parent: '^accordion' 471 | }, 472 | templateUrl: './dist/components/accordion/accordion-panel/accordion-panel.html', 473 | controller: AccordionPanel 474 | }); 475 | ``` 476 | 477 | ##### AccordionPanel Template 478 | 479 | ```html 480 | 481 |
482 |
483 |

{{$ctrl.feed.heading}}

484 |
485 |
486 |

{{$ctrl.feed.content}}

487 |
488 |
489 | ``` 490 | 491 | ##### Use of the component 492 | 493 | ``` 494 |
495 |
496 | 497 |
498 |
499 | ``` 500 | 501 | #### React migration 502 | 503 | Let's see how the accordion could be replaced by a React component 504 | 505 | ```javascript 506 | var Accordion = function (AccordionPanel) { 507 | return React.createClass({ 508 | displayName: 'Accordion', 509 | getInitialState: function () { 510 | return { 511 | selected: null 512 | }; 513 | }, 514 | render: function () { 515 | var feeds = this.props.feeds || []; 516 | var selected = this.state.selected; 517 | var self = this; 518 | return ( 519 |
520 |

{this.props.foo}

521 | {feeds.map(function (feed, index) { 522 | return ( 523 | 530 | ); 531 | })} 532 |
533 | ); 534 | }, 535 | select: function (selected) { 536 | if (selected === this.state.selected) { 537 | selected = null; 538 | } 539 | this.setState({ selected }); 540 | } 541 | }); 542 | } 543 | 544 | angular 545 | .module('app') 546 | .factory('Accordion', ['AccordionPanel', Accordion]); 547 | ``` 548 | 549 | ##### React Accordion Panel 550 | 551 | ```javascript 552 | function AccordionPanel(props) { 553 | var select = function () { 554 | return props.onSelect(props.feed.id); 555 | }; 556 | var className = 'panel-body collapsible'; 557 | if (props.active) { 558 | className += ' in'; 559 | } 560 | return ( 561 |
562 |
563 |

{props.feed.heading}

564 |
565 |
566 |

{props.feed.content}

567 |
568 |
569 | ); 570 | } 571 | 572 | AccordionPanel.displayName = 'AccordionPanel'; 573 | AccordionPanel.propTypes = { 574 | active: React.PropTypes.bool, 575 | onSelect: React.PropTypes.func, 576 | feed: React.PropTypes.shape({ 577 | id: React.PropTypes.number.isRequired, 578 | heading: React.PropTypes.string, 579 | content: React.PropTypes.string 580 | }) 581 | }; 582 | 583 | angular 584 | .module('app') 585 | .value('AccordionPanel', AccordionPanel); 586 | ``` 587 | 588 | ##### Use 589 | 590 | ```html 591 |
592 |
593 | 594 |
595 |
596 | ``` 597 | 598 | | The complete implementation can be found at [ 05.C Angular components & directive.](https://github.com/Lemoncode/integrate-react-legacy-apps/tree/master/05.C%20Angular%20components%20%26%20directive) | 599 | | :---------------------------------------------------------------------------------------------------------------------: | 600 | 601 | ## Conclusion 602 | 603 | React is a very complete and high performance library that allows very modular and scalable development, also it brings great tools for easier development like [React Developer Tools](https://github.com/facebook/react-devtools) or testing libraries like [Enzyme](https://github.com/airbnb/enzyme) or [Jest](https://facebook.github.io/jest/) to ensure application quality. 604 | Even though React itself does not provide all application parts, we have an available ecosystem that perfectly fit and cover all application development scopes like [Redux](https://github.com/reactjs/redux) for managing application state or [React-Router](https://github.com/ReactTraining/react-router) for routing and synchronizing components with navigation. Furthermore, one can reuse components to build hybrid applications with [React Native.](https://facebook.github.io/react-native/) 605 | # Integrate React legacy apps 606 | The goal of this project is provide a set of samples covering concepts to migrate from a legacy app to React. 607 | 608 | ## 00 Boilerplate 609 | 610 | Initial setup for an ES5 web application using jQuery, namespacing and module pattern. This sample shows a simple table with mocked data. 611 | 612 | ## 01.A Basic Integration 613 | 614 | This sample replaces the table from previous sample using `React.createElement` without transpilation. 615 | 616 | ## 01.B Move To JSX 617 | 618 | This sample shows how to move the React component from previous sample to JSX syntax and configure Gulp to set up automatic transpilation. 619 | 620 | ## 02 Props and Render 621 | 622 | This sample covers basic communication between jQuery and React using `React.render` to update the component properties. A form is created to add more records to the table. 623 | 624 | ## 03 Stateful Component 625 | 626 | This sample changes the component from previous sample to use the state istead of properties. The goal of this samples is to show we can use methods from component directly. 627 | 628 | ## 04 Event Emitter 629 | 630 | This sample shows how to use Publish/Subscribe pattern with `jQuery.Callbacks()` method to create a communcation layer between jQuery and React as event emitters. 631 | 632 | ## 05.A Angular controllerAS & directive 633 | 634 | This sample shows a basic Angular form and uses a React component to display a piece of the Angular model. It uses [ngReact](https://github.com/ngReact/ngReact) to use a directive that wrapps the React component. 635 | 636 | ## 05.B Angular controllerAS & factory 637 | 638 | This sampl is based on the previous sample to show how to use ngReact library to expose the React component using the ngReact *reactDirective* factory. 639 | 640 | ## 05.C Angular components & directive 641 | 642 | This sample uses how to replace an accordion of a web component based Angular application with a React accordion. it also uses ngReact for libraries communication. 643 | 644 | # About Basefactor + Lemoncode 645 | 646 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 647 | 648 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 649 | 650 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 651 | 652 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 653 | 654 | 655 | --------------------------------------------------------------------------------