├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json └── samples ├── angular-patient-demographics-example ├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .yo-rc.json ├── Gruntfile.js ├── README.md ├── app │ ├── index.html │ ├── scripts │ │ ├── app.module.js │ │ ├── common │ │ │ ├── ssn.filter.js │ │ │ └── tel.filter.js │ │ └── patient │ │ │ ├── demographics │ │ │ ├── basic │ │ │ │ ├── basic.info.controller.js │ │ │ │ ├── basic.info.directive.js │ │ │ │ ├── basic.info.view.html │ │ │ │ └── basic.module.js │ │ │ ├── contact │ │ │ │ ├── contact.info.controller.js │ │ │ │ ├── contact.info.directive.js │ │ │ │ ├── contact.info.view.html │ │ │ │ └── contact.module.js │ │ │ ├── demographics.controller.js │ │ │ ├── demographics.directive.js │ │ │ ├── demographics.module.js │ │ │ └── demographics.view.html │ │ │ ├── patient.controller.js │ │ │ ├── patient.html │ │ │ ├── patient.module.js │ │ │ └── patient.service.js │ └── styles │ │ └── main.css ├── bower.json ├── package.json └── test │ ├── .jshintrc │ ├── karma.conf.js │ └── spec │ └── patient │ ├── demographics │ ├── basic │ │ └── basic.info.controller.spec.js │ └── contact │ │ └── contact.info.controller.spec.js │ └── patient.service.spec.js └── react-redux-patient-demographics-example ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── bin ├── compile.js └── dev-server.js ├── config ├── environments.config.js ├── karma.config.js ├── project.config.js └── webpack.config.js ├── package.json ├── postcss.config.js ├── public └── robots.txt ├── server └── main.js ├── src ├── common │ ├── CustomValidators.js │ ├── Formatters.js │ ├── FormsyDatePicker.js │ ├── FormsyHiddenInput.js │ ├── FormsyInput.js │ ├── FormsyMaskedInput.js │ └── FormsySelect.js ├── containers │ └── AppContainer.js ├── index.html ├── layouts │ └── CoreLayout │ │ ├── CoreLayout.js │ │ ├── CoreLayout.scss │ │ └── index.js ├── main.js ├── routes │ ├── Patient │ │ ├── Demographics │ │ │ ├── Basic │ │ │ │ └── BasicComponent.js │ │ │ ├── Contact │ │ │ │ └── ContactComponent.js │ │ │ ├── PatientDemographicsComponent.js │ │ │ └── PatientDemographicsContainer.js │ │ ├── PatientModule.js │ │ └── index.js │ └── index.js ├── store │ ├── createStore.js │ ├── location.js │ └── reducers.js └── styles │ ├── _base.scss │ └── core.scss ├── tests ├── .eslintrc ├── test-bundler.js └── test.spec.js ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | before_script: 5 | - npm install -g grunt-cli bower 6 | notifications: 7 | slack: goteamepsilon:wId4MyJvwocMrg7gOzxov00k 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 TeamEpsilon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/GoTeamEpsilon/angular-to-react-redux.svg?branch=master)](https://travis-ci.org/GoTeamEpsilon/angular-to-react-redux) 2 | 3 | ![img](http://i.imgur.com/Gq1eJpa.png) 4 | 5 | This repository is an educational resource for Angular v1 experts that are looking to learn React/Redux. A contrived sample application for managing basic patient information is provided using both technologies' best practices (look under `samples/` to see both versions). This application (demonstrated below) will be referred to below as we explore the key philosophical differences between Angular v1 and React/Redux so that you can get coding! 6 | 7 | ![gif](https://i.imgur.com/TGUAuCF.gif) 8 | 9 | Getting Started 10 | =============== 11 | 12 | ### 🔨 Scaffolding & Tooling 13 | 14 | Once the technologies for a project are chosen, the next step is to figure out how to scaffold and build the application using production-ready practices. Angular v1 applications are typically wired together using a mixture of [NPM](https://www.npmjs.com/) and [Bower](https://bower.io/) (dependency management) and [Grunt](https://gruntjs.com/) or [Gulp](http://gulpjs.com/) (build tooling). In the React/Redux world, NPM and [Webpack](https://webpack.github.io/) are the way to go (this repo uses and recommends the [react-redux-starter-kit](https://github.com/davezuko/react-redux-starter-kit) which incorporates these technologies). 15 | 16 | Scaffolding a project in React/Redux isn't very different from what is typically done in Angular v1 (with some exceptions to the [John Papa styleguide](https://github.com/johnpapa/angular-styleguide)). Here's the `tree` output of both samples in this repository: 17 | 18 | ``` 19 | Angular v1 React/Redux 20 | ------------ ------------- 21 | ├── app.module.js ├── common 22 | ├── common │ ├── CustomValidators.js 23 | │ ├── ssn.filter.js │ ├── Formatters.js 24 | │ └── tel.filter.js │ ├── FormsyDatePicker.js 25 | ├── patient │ ├── FormsyInput.js 26 | │ ├── demographics │ └── FormsyMaskedInput.js 27 | │ │ ├── basic ├── containers 28 | │ │ │ ├── basic.info.controller.js │ └── AppContainer.js 29 | │ │ │ ├── basic.info.directive.js ├── index.html 30 | │ │ │ ├── basic.info.view.html ├── layouts 31 | │ │ │ └── basic.module.js │ └── CoreLayout 32 | │ │ ├── contact │ ├── CoreLayout.js 33 | │ │ │ ├── contact.info.controller.js │ ├── CoreLayout.scss 34 | │ │ │ ├── contact.info.directive.js │ └── index.js 35 | │ │ │ ├── contact.info.view.html ├── main.js 36 | │ │ │ └── contact.module.js ├── routes 37 | │ │ ├── demographics.controller.js │ ├── index.js 38 | │ │ ├── demographics.directive.js │ └── Patient 39 | │ │ ├── demographics.module.js │ ├── Demographics 40 | │ │ └── demographics.view.html │ │ ├── Basic 41 | │ ├── patient.controller.js │ │ │ └── BasicComponent.js 42 | │ ├── patient.html │ │ ├── Contact 43 | │ ├── patient.module.js │ │ │ └── ContactComponent.js 44 | │ └── patient.service.js │ │ │ 45 | └── index.html │ │ ├── PatientDemographicsComponent.js 46 | │ │ └── PatientDemographicsContainer.js 47 | │ ├── index.js 48 | │ └── PatientModule.js 49 | ├── store 50 | │ ├── createStore.js 51 | │ ├── location.js 52 | │ └── reducers.js 53 | └── styles 54 | ├── _base.scss 55 | └── core.scss 56 | ``` 57 | 58 | Notice how everything is [organized in modules](https://medium.com/@scbarrus/the-ducks-file-structure-for-redux-d63c41b7035c#.ji6r2j61o) as opposed to a flat directory approach. This is a best practice that helps one organize a complex user interface while still sharing generic pieces. 59 | 60 | Now that the file structure (hopefully) makes sense, one can go back a directory and run the build tool (you won't find major differences between gulp/grunt and webpack). In our case, it's `grunt serve` and `npm start` for Angular v1 and React/Redux samples, respectively. 61 | 62 | ### 🎛 Directives vs Components 63 | 64 | The heart of Angular v1 is with directives. These discrete interfaces take in 1 and 2-way data parameters and inject services that really power up your view. Fortunately, directives are not that different from Redux-aware React components. Moreover, the stuff inside of React components can be easily translated from Angular v1 concepts (this repo won't go into those details, as they are easily "Googleable"... for instance Google _"React equivalent for ng-repeat"_ to see for yourself). 65 | 66 | In Angular v1, directives are typically introduced in views that are controlled by route-level controllers. In React/Redux, components are introduced in the same way, however, a container must be placed in the middle so that the component (also known as a [smart component](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.htl1bug49), in this case) can get application-wide state. The container will also bring in functions from upper level services that child components will use. These topics (application-wide state and upper level services) will be explained later in the guide. 67 | 68 | As mentioned before, Redux has to bind its store to child React components in a container. In our application, the `routes/Patient/Demographics/PatientDemographicsContainer.js` puts references to the Redux state like so (code simplified for sake of demonstration): 69 | 70 | ```jsx 71 | const mapStateToProps = (state) => ({ 72 | patientInContext: state.patient.patientInContext, 73 | basic: state.patient[state.patient.patientInContext].basic 74 | contacts: state.patient[state.patient.patientInContext].contact 75 | }) 76 | ``` 77 | 78 | One other important thing that is done in this container is the binding of Redux-aware service functions (formally known as "action creators") like so: 79 | 80 | ```jsx 81 | const mapDispatchToProps = { 82 | setPatientInContext, 83 | updatePatientData, 84 | updateContactData, 85 | deleteContact, 86 | startAddingNewContact 87 | } 88 | ``` 89 | 90 | With these two mappings out of the way, child components can access the state and functions from above. Here are some highlighted examples of this in `routes/Patient/Demographics/Basic/BasicComponent.js`: 91 | 92 | Displaying data from the store in a table: 93 | ```jsx 94 | 95 | SSN: {socialSecurityFormat(this.props.info.ssn)} 96 | Martial Status: {this.props.info.martialStatus} 97 | 98 | ``` 99 | 100 | Form edit reference to the SSN input (uses local component state instead of Redux store state - note that [Formsy](https://github.com/christianalfoni/formsy-react) is a popular form validation library): 101 | ```jsx 102 | 103 | 117 | 118 | ``` 119 | 120 | Save function for the form that takes the local component state of the form and sends it to the Redux store for updating. Note that `updatePatientData` is passed from the parent container: 121 | ```jsx 122 | handleSubmit(formValues) { 123 | // Convert dob back to date string 124 | formValues.dob = formValues.dob.format('YYYY-MM-DD') 125 | this.props.updatePatientData(formValues) 126 | this.setState({ showForm: false }) 127 | } 128 | ``` 129 | 130 | At this point, you may be thinking _"wait, why are you copying the data from Redux into the local state/form rather than using it directly? Isn't the point of Redux to encapsulate _all_ application state?"_. Good question. As with most things in software engineering, it is always best to be able to break the rules when it's justified. Should state such as `{ showForm: true/false }` (determines whether to render the form or not) and `{ cachedForm: this.props.info }` (holds a cache of the form state if the user hits cancel) be put into the Redux store or just be local to the component? It depends. In our case, this state doesn't need to be application wide so Redux is only storing things that are domain-centric rather than domain-centric and UI-centric. These things will often come down to requirements and what the opinions are of your resident seasoned Redux enthusiast(s). 131 | 132 | Service Layer 133 | ============= 134 | 135 | ### 🌿 Store 136 | 137 | In Angular v1, application-wide state is put into services so that directive controllers can CRUD it. In React/Redux, all application-wide state is put into the store, an object tree. As shown in the above section, components access the store via containers and parent components passing it to them. Components can alter said state by invoking module functions (formally known as "action creators") that containers and parent components pass down. 138 | 139 | One key difference between Angular v1 application-wide state in services and Redux store is that state mutation is not allowed. While this sounds weird and scary at first, you _can_ change this state but it must be done in a very specific way. The easiest way to think about this is whenever you update the store, you simply clone the object, mutate the clone to your heart's content, and send that to the store. 140 | 141 | Think back to your Angular v1 directives that display information from a service that holds the state. When that service state changes, the directive will change the view to reflect said change. Similarly, Redux-aware React components will change when the store changes. 142 | 143 | ### ✨ Actions & Pure Reducers 144 | 145 | A key difference between the updating of the state in an Angular v1 service and in the Redux store is that you don't "talk" to the store directly. In order to get the store to respond to data changes, you must issue an action. Actions simply send data from your application to your store and then your app "reacts" (pardon the pun). 146 | 147 | Recall that the `routes/Patient/Demographics/Basic/BasicComponent.js` calls `this.props.updatePatientData(formValues)` when it wishes to update basic patient information in the store. The `updatePatientData` function is defined in the module `routes/Patient/PatientModule.js` (modules will be covered in the next section) that looks like this: 148 | 149 | ```jsx 150 | export const updatePatientData = (data) => { 151 | return (dispatch, getState) => { 152 | return new Promise((resolve, reject) => { 153 | console.debug(`updating basic patient data for ${getState().patient.patientInContext}`) 154 | dispatch({ 155 | type : 'UPDATE_PATIENT_DATA', 156 | payload : data 157 | }) 158 | resolve() 159 | }) 160 | } 161 | } 162 | ``` 163 | 164 | The important piece to focus on for now is the `dispatch` function. This function takes in something called an action. In our case, our action is of type `UPDATE_PATIENT_DATA` and the payload is the new basic data. 165 | 166 | When the action has been dispatched, something needs to handle it so that the store is updated. This is the job of the reducer. Reducers look at an inbound action and figure out how to update the store with the new information. For example `routes/Patient/PatientModule.js` exposes the following reducer: 167 | 168 | ```jsx 169 | const initialState = testData 170 | export default function patientReducer (state = initialState, action) { 171 | let result 172 | let copy 173 | switch (action.type) { 174 | case 'UPDATE_PATIENT_IN_CONTEXT': 175 | copy = clone(state) 176 | copy.patientInContext = action.payload 177 | result = copy 178 | break 179 | case 'UPDATE_PATIENT_DATA': 180 | copy = clone(state) 181 | copy[copy.patientInContext].basic = action.payload 182 | result = copy 183 | break 184 | case 'UPDATE_CONTACT_DATA': 185 | copy = clone(state) 186 | const contactIndexForUpdation = _.findIndex(copy[copy.patientInContext].contacts, (c) => { 187 | if (c && c.hasOwnProperty('id')) { 188 | return c.id === action.payload.id 189 | } 190 | }) 191 | copy[copy.patientInContext].contacts[contactIndexForUpdation] = action.payload 192 | result = copy 193 | break 194 | case 'INSERT_CONTACT': 195 | copy = clone(state) 196 | const lastContact = _.last(copy[copy.patientInContext].contacts) 197 | let newContactId = 0 198 | if (lastContact != null && lastContact.hasOwnProperty('id')) { 199 | newContactId = lastContact.id + 1 200 | } 201 | copy[copy.patientInContext].contacts.push({ isNewContact: true, id: newContactId }) 202 | result = copy 203 | break 204 | case 'DELETE_CONTACT': 205 | copy = clone(state) 206 | const contactIndexForDeletion = _.findIndex(copy[copy.patientInContext].contacts, (c) => { 207 | if (c && c.hasOwnProperty('id')) { 208 | return c.id === action.payload 209 | } 210 | }) 211 | delete copy[copy.patientInContext].contacts[contactIndexForDeletion] 212 | result = copy 213 | break 214 | default: 215 | result = state 216 | } 217 | 218 | return result 219 | } 220 | ``` 221 | 222 | There is a good amount going on here, but the most important thing to focus on is that `state` variable. This is the application store. Because we are `switch`ing on action types, the reducer will know to run the code in the `UPDATE_PATIENT_DATA` section. This code is simply making a copy of the store and editing it with the new basic information. At the end of the function `return result` is called and the Redux store is updated. 223 | 224 | What's interesting is that the reducer is [pure](https://www.sitepoint.com/functional-programming-pure-functions/) in that no mutations were made to the original store because [clone](https://www.npmjs.com/package/clone) (a nice NPM module) copied the store to a new object. 225 | 226 | ### 🏭 Modules 227 | 228 | In the last section, we learned that `routes/Patient/Demographics/Basic/BasicComponent.js` calls `this.props.updatePatientData(formValues)` which dispatches an action to the reducer so that the store can be updated. You may be thinking _"the module is just a place where actions are created and reducers run based on those actions"_. This is correct, but there is an additional piece worth pointing out. Modules can serve as a centralized place for logic to run before the store updates. 229 | 230 | `routes/Patient/Demographics/Contact/ContactComponent.js` allows users to add a new contact. This data payload will eventually make it up to the module and the module may wish to perform an HTTP `POST` to a server (note that our sample application doesn't do this) before saving the new contact information to the store. This logic is totally appropriate for the module function and would look something like this: 231 | 232 | ```jsx 233 | export const startAddingNewContact = (data) => { 234 | return (dispatch, getState) => { 235 | return HttpPost(endpoint, data) 236 | .then((response) => { 237 | data.id = response.data.id 238 | dispatch({ 239 | type : 'INSERT_CONTACT', 240 | payload : data 241 | }) 242 | }) 243 | }) 244 | } 245 | } 246 | ``` 247 | 248 | You may be thinking _"I see there's a mutation here (function is not pure)... `data` is getting an `id` added on, is that allowable?"_. The answer is "yes". Module functions can be asynchronous and have side effects. The important thing is that the reducer that will receive the action will be pure and synchronous. 249 | 250 | Unit Testing 251 | ============ 252 | 253 | ### 🔬 Frameworks & Philosophy 254 | 255 | Unit testing is not too much different than the approaches found in Angular v1. [Karma](https://karma-runner.github.io/1.0/index.html), [Mocha](https://mochajs.org/), [Chai](http://chaijs.com/), and [Sinon](http://sinonjs.org/) are used as the test runner, framework, assertion library, and mocks/spies tools respectively. 256 | 257 | The only philosophical difference that is notable is that tests assert items on the component view in React/Redux. This is typically not done in Angular v1 unit tests. 258 | 259 | NOTE: `v1.0.0` Didn't include unit tests for the Redux/React sample application (embarrasing, right?). The Angular v1 sample tests are in place, but we plan on doing the Redux/React tests in `v1.0.1`. 260 | 261 | Additional Resources 262 | ==================== 263 | 264 | - [React Component Lifecycle](http://busypeoples.github.io/post/react-component-lifecycle/) 265 | - [The Difference Between Virtual DOM and DOM](http://reactkungfu.com/2015/10/the-difference-between-virtual-dom-and-dom/) 266 | - [1-way vs 2-way Databinding](http://stackoverflow.com/a/37566693/1525534) 267 | - [Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.htl1bug49) 268 | - [React Global Error Handling](http://stackoverflow.com/a/31112522/1525534) 269 | - [Redux Logger](https://github.com/evgenyrodionov/redux-logger) 270 | - [Redux Ducks File Structure](https://medium.com/@scbarrus/the-ducks-file-structure-for-redux-d63c41b7035c#.ji6r2j61o) 271 | - [React Logger](https://www.npmjs.com/package/react-logger) 272 | - [ES6 Highlights](https://pure-essence.net/2015/11/29/javascript-es6-highlights/) 273 | - [React/Redux Router Tutorial](https://github.com/reactjs/react-router-redux#tutorial) 274 | - [Redux Middleware](http://redux.js.org/docs/advanced/Middleware.html) 275 | - [Redux Wes Box Redux Tutorials](https://www.youtube.com/watch?v=hmwBow1PUuo&list=PLu8EoSxDXHP5uyzEWxdlr9WQTJJIzr6jy) 276 | - [Master Redux Resources List](https://github.com/xgrommx/awesome-redux) 277 | 278 | ## License & Credits 279 | 280 | - [MIT](LICENSE) 281 | - [Victor Choueiri](https://github.com/xqwzts), who reviewed the code 282 | - [Manuel Bieh](https://github.com/manuelbieh), who reviewed the code 283 | - [Google font used for header](https://fonts.google.com/specimen/Fjalla+One) 284 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-to-react-redux", 3 | "version": "1.0.0", 4 | "description": "This repository is an educational resource for Angular v1 experts that are looking to learn React/Redux. Edit", 5 | "main": "index.js", 6 | "scripts": { 7 | "angular": "cd samples/angular-patient-demographics-example && npm install && bower install && grunt", 8 | "reactredux": "cd samples/react-redux-patient-demographics-example && npm install && npm run lint && npm run test", 9 | "test": "concurrent \"npm run angular\" \"npm run reactredux\" " 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/GoTeamEpsilon/angular-to-react-redux.git" 14 | }, 15 | "author": "Team Epsilon", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "concurrently": "^1.0.0" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/GoTeamEpsilon/angular-to-react-redux/issues" 22 | }, 23 | "homepage": "https://github.com/GoTeamEpsilon/angular-to-react-redux#readme" 24 | } 25 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /.tmp 4 | /.sass-cache 5 | /bower_components 6 | /coverage 7 | 8 | # For Webstorm 9 | .idea 10 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCamelCaseOrUpperCaseIdentifiers": true, 3 | "requireCapitalizedConstructors": true, 4 | "requireParenthesesAroundIIFE": true, 5 | "validateQuoteMarks": "'" 6 | } 7 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "esnext": true, 7 | "latedef": true, 8 | "noarg": true, 9 | "node": true, 10 | "strict": true, 11 | "undef": true, 12 | "unused": true, 13 | "latedef": "nofunc", 14 | "globals": { 15 | "angular": false, 16 | "alert": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-karma": { 3 | "base-path": "../", 4 | "frameworks": "jasmine", 5 | "browsers": "PhantomJS", 6 | "app-files": "app/scripts/**/*.js", 7 | "files-comments": "bower:js,endbower", 8 | "bower-components-path": "bower_components", 9 | "test-files": "test/mock/**/*.js,test/spec/**/*.js" 10 | } 11 | } -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Somewhat modified for John Papa style guide compliance 2017-01-16 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Time how long tasks take. Can help when optimizing build times 13 | require('time-grunt')(grunt); 14 | 15 | // Automatically load required Grunt tasks 16 | require('jit-grunt')(grunt, { 17 | useminPrepare: 'grunt-usemin', 18 | ngtemplates: 'grunt-angular-templates', 19 | cdnify: 'grunt-google-cdn' 20 | }); 21 | 22 | // Configurable paths for the application 23 | var appConfig = { 24 | app: require('./bower.json').appPath || 'app', 25 | dist: 'dist' 26 | }; 27 | 28 | // Define the configuration for all the tasks 29 | grunt.initConfig({ 30 | 31 | // Project settings 32 | yeoman: appConfig, 33 | 34 | // Watches files for changes and runs tasks based on the changed files 35 | watch: { 36 | bower: { 37 | files: ['bower.json'], 38 | tasks: ['wiredep'] 39 | }, 40 | js: { 41 | files: ['<%= yeoman.app %>/scripts/{,**/}*.js'], 42 | tasks: ['newer:jshint:all', 'newer:jscs:all'], 43 | options: { 44 | livereload: '<%= connect.options.livereload %>' 45 | } 46 | }, 47 | jsTest: { 48 | files: ['test/spec/{,**/}*.js'], 49 | tasks: ['newer:jshint:test', 'newer:jscs:test', 'karma'] 50 | }, 51 | styles: { 52 | files: ['<%= yeoman.app %>/styles/{,*/}*.css'], 53 | tasks: ['newer:copy:styles', 'postcss'] 54 | }, 55 | gruntfile: { 56 | files: ['Gruntfile.js'] 57 | }, 58 | livereload: { 59 | options: { 60 | livereload: '<%= connect.options.livereload %>' 61 | }, 62 | files: [ 63 | '<%= yeoman.app %>/{,*/}*.html', 64 | '.tmp/styles/{,*/}*.css', 65 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 66 | ] 67 | } 68 | }, 69 | 70 | // The actual grunt server settings 71 | connect: { 72 | options: { 73 | port: 9000, 74 | // Change this to '0.0.0.0' to access the server from outside. 75 | hostname: 'localhost', 76 | livereload: 35729 77 | }, 78 | livereload: { 79 | options: { 80 | open: true, 81 | middleware: function (connect) { 82 | return [ 83 | connect.static('.tmp'), 84 | connect().use( 85 | '/bower_components', 86 | connect.static('./bower_components') 87 | ), 88 | connect().use( 89 | '/app/styles', 90 | connect.static('./app/styles') 91 | ), 92 | connect.static(appConfig.app) 93 | ]; 94 | } 95 | } 96 | }, 97 | test: { 98 | options: { 99 | port: 9001, 100 | middleware: function (connect) { 101 | return [ 102 | connect.static('.tmp'), 103 | connect.static('test'), 104 | connect().use( 105 | '/bower_components', 106 | connect.static('./bower_components') 107 | ), 108 | connect.static(appConfig.app) 109 | ]; 110 | } 111 | } 112 | }, 113 | dist: { 114 | options: { 115 | open: true, 116 | base: '<%= yeoman.dist %>' 117 | } 118 | } 119 | }, 120 | 121 | // Make sure there are no obvious mistakes 122 | jshint: { 123 | options: { 124 | jshintrc: '.jshintrc', 125 | reporter: require('jshint-stylish') 126 | }, 127 | all: { 128 | src: [ 129 | 'Gruntfile.js', 130 | '<%= yeoman.app %>/scripts/{,**/}*.js' 131 | ] 132 | }, 133 | test: { 134 | options: { 135 | jshintrc: 'test/.jshintrc' 136 | }, 137 | src: ['test/spec/{,**/}*.js'] 138 | } 139 | }, 140 | 141 | // Make sure code styles are up to par 142 | jscs: { 143 | options: { 144 | config: '.jscsrc', 145 | verbose: true 146 | }, 147 | all: { 148 | src: [ 149 | 'Gruntfile.js', 150 | '<%= yeoman.app %>/scripts/{,**/}*.js' 151 | ] 152 | }, 153 | test: { 154 | src: ['test/spec/{,**/}*.js'] 155 | } 156 | }, 157 | 158 | // Empties folders to start fresh 159 | clean: { 160 | dist: { 161 | files: [{ 162 | dot: true, 163 | src: [ 164 | '.tmp', 165 | '<%= yeoman.dist %>/{,*/}*', 166 | '!<%= yeoman.dist %>/.git{,*/}*' 167 | ] 168 | }] 169 | }, 170 | server: '.tmp' 171 | }, 172 | 173 | // Add vendor prefixed styles 174 | postcss: { 175 | options: { 176 | processors: [ 177 | require('autoprefixer-core')({browsers: ['last 1 version']}) 178 | ] 179 | }, 180 | server: { 181 | options: { 182 | map: true 183 | }, 184 | files: [{ 185 | expand: true, 186 | cwd: '.tmp/styles/', 187 | src: '{,*/}*.css', 188 | dest: '.tmp/styles/' 189 | }] 190 | }, 191 | dist: { 192 | files: [{ 193 | expand: true, 194 | cwd: '.tmp/styles/', 195 | src: '{,*/}*.css', 196 | dest: '.tmp/styles/' 197 | }] 198 | } 199 | }, 200 | 201 | // Automatically inject Bower components into the app 202 | wiredep: { 203 | app: { 204 | src: ['<%= yeoman.app %>/index.html'], 205 | ignorePath: /\.\.\// 206 | }, 207 | test: { 208 | devDependencies: true, 209 | src: '<%= karma.unit.configFile %>', 210 | ignorePath: /\.\.\//, 211 | fileTypes:{ 212 | js: { 213 | block: /(([\s\t]*)\/{2}\s*?bower:\s*?(\S*))(\n|\r|.)*?(\/{2}\s*endbower)/gi, 214 | detect: { 215 | js: /'(.*\.js)'/gi 216 | }, 217 | replace: { 218 | js: '\'{{filePath}}\',' 219 | } 220 | } 221 | } 222 | } 223 | }, 224 | 225 | // Renames files for browser caching purposes 226 | filerev: { 227 | dist: { 228 | src: [ 229 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 230 | '<%= yeoman.dist %>/styles/{,*/}*.css', 231 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 232 | '<%= yeoman.dist %>/styles/fonts/*' 233 | ] 234 | } 235 | }, 236 | 237 | // Reads HTML for usemin blocks to enable smart builds that automatically 238 | // concat, minify and revision files. Creates configurations in memory so 239 | // additional tasks can operate on them 240 | useminPrepare: { 241 | html: '<%= yeoman.app %>/index.html', 242 | options: { 243 | dest: '<%= yeoman.dist %>', 244 | flow: { 245 | html: { 246 | steps: { 247 | js: ['concat', 'uglifyjs'], 248 | css: ['cssmin'] 249 | }, 250 | post: {} 251 | } 252 | } 253 | } 254 | }, 255 | 256 | // Performs rewrites based on filerev and the useminPrepare configuration 257 | usemin: { 258 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 259 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 260 | js: ['<%= yeoman.dist %>/scripts/{,*/}*.js'], 261 | options: { 262 | assetsDirs: [ 263 | '<%= yeoman.dist %>', 264 | '<%= yeoman.dist %>/images', 265 | '<%= yeoman.dist %>/styles' 266 | ], 267 | patterns: { 268 | js: [[/(images\/[^''""]*\.(png|jpg|jpeg|gif|webp|svg))/g, 'Replacing references to images']] 269 | } 270 | } 271 | }, 272 | 273 | // The following *-min tasks will produce minified files in the dist folder 274 | // By default, your `index.html`'s will take care of 275 | // minification. These next options are pre-configured if you do not wish 276 | // to use the Usemin blocks. 277 | // cssmin: { 278 | // dist: { 279 | // files: { 280 | // '<%= yeoman.dist %>/styles/main.css': [ 281 | // '.tmp/styles/{,*/}*.css' 282 | // ] 283 | // } 284 | // } 285 | // }, 286 | // uglify: { 287 | // dist: { 288 | // files: { 289 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 290 | // '<%= yeoman.dist %>/scripts/scripts.js' 291 | // ] 292 | // } 293 | // } 294 | // }, 295 | // concat: { 296 | // dist: {} 297 | // }, 298 | 299 | imagemin: { 300 | dist: { 301 | files: [{ 302 | expand: true, 303 | cwd: '<%= yeoman.app %>/images', 304 | src: '{,*/}*.{png,jpg,jpeg,gif}', 305 | dest: '<%= yeoman.dist %>/images' 306 | }] 307 | } 308 | }, 309 | 310 | svgmin: { 311 | dist: { 312 | files: [{ 313 | expand: true, 314 | cwd: '<%= yeoman.app %>/images', 315 | src: '{,*/}*.svg', 316 | dest: '<%= yeoman.dist %>/images' 317 | }] 318 | } 319 | }, 320 | 321 | htmlmin: { 322 | dist: { 323 | options: { 324 | collapseWhitespace: true, 325 | conservativeCollapse: true, 326 | collapseBooleanAttributes: true, 327 | removeCommentsFromCDATA: true 328 | }, 329 | files: [{ 330 | expand: true, 331 | cwd: '<%= yeoman.dist %>', 332 | src: ['*.html'], 333 | dest: '<%= yeoman.dist %>' 334 | }] 335 | } 336 | }, 337 | 338 | ngtemplates: { 339 | dist: { 340 | options: { 341 | module: 'patientDemographicsExampleApp', 342 | htmlmin: '<%= htmlmin.dist.options %>', 343 | usemin: 'scripts/scripts.js' 344 | }, 345 | cwd: '<%= yeoman.app %>', 346 | src: '/scripts/{,**/}*.html', 347 | dest: '.tmp/templateCache.js' 348 | } 349 | }, 350 | 351 | // ng-annotate tries to make the code safe for minification automatically 352 | // by using the Angular long form for dependency injection. 353 | ngAnnotate: { 354 | dist: { 355 | files: [{ 356 | expand: true, 357 | cwd: '.tmp/concat/scripts', 358 | src: '*.js', 359 | dest: '.tmp/concat/scripts' 360 | }] 361 | } 362 | }, 363 | 364 | // Replace Google CDN references 365 | cdnify: { 366 | dist: { 367 | html: ['<%= yeoman.dist %>/*.html'] 368 | } 369 | }, 370 | 371 | // Copies remaining files to places other tasks can use 372 | copy: { 373 | dist: { 374 | files: [{ 375 | expand: true, 376 | dot: true, 377 | cwd: '<%= yeoman.app %>', 378 | dest: '<%= yeoman.dist %>', 379 | src: [ 380 | '*.{png,txt}', 381 | '*.html', 382 | 'scripts/{,**/}*.html', 383 | 'images/{,*/}*.{webp}', 384 | 'styles/fonts/{,*/}*.*' 385 | ] 386 | }, { 387 | expand: true, 388 | cwd: '.tmp/images', 389 | dest: '<%= yeoman.dist %>/images', 390 | src: ['generated/*'] 391 | }, { 392 | expand: true, 393 | cwd: 'bower_components/bootstrap/dist', 394 | src: 'fonts/*', 395 | dest: '<%= yeoman.dist %>' 396 | }] 397 | }, 398 | styles: { 399 | expand: true, 400 | cwd: '<%= yeoman.app %>/styles', 401 | dest: '.tmp/styles/', 402 | src: '{,*/}*.css' 403 | } 404 | }, 405 | 406 | // Run some tasks in parallel to speed up the build process 407 | concurrent: { 408 | server: [ 409 | 'copy:styles' 410 | ], 411 | test: [ 412 | 'copy:styles' 413 | ], 414 | dist: [ 415 | 'copy:styles', 416 | 'imagemin', 417 | 'svgmin' 418 | ] 419 | }, 420 | 421 | // Test settings 422 | karma: { 423 | unit: { 424 | configFile: 'test/karma.conf.js', 425 | singleRun: true 426 | } 427 | } 428 | }); 429 | 430 | 431 | grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { 432 | if (target === 'dist') { 433 | return grunt.task.run(['build', 'connect:dist:keepalive']); 434 | } 435 | 436 | grunt.task.run([ 437 | 'clean:server', 438 | 'wiredep', 439 | 'concurrent:server', 440 | 'postcss:server', 441 | 'connect:livereload', 442 | 'watch' 443 | ]); 444 | }); 445 | 446 | grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) { 447 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 448 | grunt.task.run(['serve:' + target]); 449 | }); 450 | 451 | grunt.registerTask('test', [ 452 | 'clean:server', 453 | 'wiredep', 454 | 'concurrent:test', 455 | 'postcss', 456 | 'connect:test', 457 | 'karma' 458 | ]); 459 | 460 | grunt.registerTask('build', [ 461 | 'clean:dist', 462 | 'wiredep', 463 | 'useminPrepare', 464 | 'concurrent:dist', 465 | 'postcss', 466 | 'ngtemplates', 467 | 'concat', 468 | 'ngAnnotate', 469 | 'copy:dist', 470 | 'cdnify', 471 | 'cssmin', 472 | 'uglify', 473 | 'filerev', 474 | 'usemin', 475 | 'htmlmin' 476 | ]); 477 | 478 | grunt.registerTask('default', [ 479 | 'newer:jshint', 480 | 'newer:jscs', 481 | 'test', 482 | 'build' 483 | ]); 484 | }; 485 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/README.md: -------------------------------------------------------------------------------- 1 | # Angular Patient Demographics Example Project 2 | 3 | Sample Angular app to be used as a baseline for a React/Redux port. Adheres to community best practices. 4 | 5 | ## Dev 6 | 7 | Install via `npm install & bower install` 8 | 9 | Run `grunt serve` and `grunt test` 10 | 11 | ## Prod 12 | 13 | Run `grunt build` 14 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 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 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/app.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp', [ 6 | 'ngAnimate', 7 | 'ngCookies', 8 | 'ngResource', 9 | 'ngRoute', 10 | 'ngSanitize', 11 | 'ngTouch', 12 | '720kb.datepicker', 13 | 'ui.mask', 14 | 'angular-logger', // enhances `$log` to have a better format/context-awareness 15 | 'patientDemographicsExampleApp.patient' 16 | ]); 17 | })(); 18 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/common/ssn.filter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp') 6 | .filter('ssnFilter', ssnFilter); 7 | 8 | function ssnFilter() { 9 | // In the return function, we must pass in a single parameter which will be the data we will work on. 10 | // We have the ability to support multiple other parameters that can be passed into the filter optionally 11 | return function(input) { 12 | if (!input) { 13 | return; 14 | } 15 | 16 | var outputString; 17 | 18 | if (input.length < 9) { 19 | return input; 20 | } else { 21 | var outputTempString = input.toString(); 22 | outputString = outputTempString.substr(0,3) + '-' + outputTempString.substr(3,2) + '-' + outputTempString.substr(5,4); 23 | } 24 | 25 | return outputString; 26 | }; 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/common/tel.filter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp') 6 | .filter('telFilter', telFilter); 7 | 8 | function telFilter() { 9 | // In the return function, we must pass in a single parameter which will be the data we will work on. 10 | // We have the ability to support multiple other parameters that can be passed into the filter optionally 11 | return function(input) { 12 | if (!input) { 13 | return; 14 | } 15 | 16 | if(input.toString().length === 10 ) { 17 | var outputTempString = input.toString(); 18 | var outputString = outputTempString.substr(0,3) + '-' + outputTempString.substr(3,3) + '-' + outputTempString.substr(6,4); 19 | return outputString; 20 | } else { 21 | return input.toString(); 22 | } 23 | }; 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/basic/basic.info.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient.demographics.basic') 6 | .controller('BasicInfoController', BasicInfoController); 7 | 8 | BasicInfoController.$inject = ['$log', '$scope']; 9 | 10 | function BasicInfoController($log, $scope) { 11 | var logger = $log.getInstance('BasicInfoController'); 12 | 13 | var vm = this; 14 | 15 | // Used for "edit" -> "cancel" reverts 16 | var cachedDataForEditMode = null; 17 | 18 | vm.emailPattern = '.*@.*'; 19 | 20 | vm.genders = { 21 | Male: 'Male', 22 | Female: 'Female', 23 | Other: 'Other' 24 | }; 25 | 26 | vm.mode = null; 27 | 28 | vm.MODES = { 29 | READ: 'read', 30 | SAVE: 'save', 31 | EDIT: 'edit', 32 | CANCEL: 'cancel' 33 | }; 34 | 35 | vm.inEditMode = function() { 36 | return vm.mode === vm.MODES.EDIT; 37 | }; 38 | 39 | vm.inReadMode = function() { 40 | return vm.mode === vm.MODES.READ; 41 | }; 42 | 43 | vm.changeMode = function(newMode) { 44 | logger.debug('Requesting mode be changed to ' + newMode); 45 | 46 | switch(newMode) { 47 | case vm.MODES.CANCEL: 48 | handleCancelMode(); 49 | break; 50 | case vm.MODES.EDIT: 51 | handleEditMode(); 52 | break; 53 | case vm.MODES.SAVE: 54 | case vm.MODES.READ: 55 | handleSaveMode(); 56 | break; 57 | } 58 | }; 59 | 60 | function handleEditMode() { 61 | logger.debug('Caching previous contact state'); 62 | 63 | cachedDataForEditMode = angular.copy(vm.basic); 64 | vm.mode = vm.MODES.EDIT; 65 | } 66 | 67 | function handleCancelMode() { 68 | logger.debug('Applying previous contact state cache'); 69 | 70 | angular.copy(cachedDataForEditMode, vm.basic); 71 | vm.mode = vm.MODES.READ; 72 | } 73 | 74 | function handleSaveMode() { 75 | vm.mode = vm.MODES.READ; 76 | 77 | logger.debug('Releasing previous contact state cache'); 78 | cachedDataForEditMode = null; 79 | 80 | // In a real app, the `PatientService.upsertBasicInfo` would be called 81 | } 82 | 83 | function construct() { 84 | logger.debug('Constructing basic info directive'); 85 | 86 | vm.changeMode(vm.MODES.READ); // Default mode 87 | 88 | vm.initialDate = angular.copy(vm.basic.dob); 89 | } 90 | 91 | // vm.basic is not immediately available. Wait for it to be passed. 92 | var unregister = $scope.$watch(angular.bind(vm, function() { 93 | return this.basic; 94 | }), function (newVal) { 95 | if (newVal) { 96 | unregister(); 97 | construct(); 98 | } 99 | }); 100 | } 101 | })(); 102 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/basic/basic.info.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient.demographics.basic') 6 | .directive('basicInfo', function() { 7 | return { 8 | templateUrl: 'scripts/patient/demographics/basic/basic.info.view.html', 9 | restrict: 'E', 10 | link: function() {}, 11 | scope: { 12 | 'basic': '=' 13 | }, 14 | controllerAs: 'vm', 15 | bindToController: true, 16 | controller: 'BasicInfoController' 17 | }; 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/basic/basic.info.view.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Name: {{vm.basic.name}}DOB: {{vm.basic.dob}}
S.S.: {{vm.basic.ss | ssnFilter}}Martial Status: {{vm.basic.martialStatus}}
Gender: {{vm.basic.gender}} Address: {{vm.basic.address}}
City: {{vm.basic.city}}Postal: {{vm.basic.postal}}
State: {{vm.basic.state}}Country: {{vm.basic.country}}
Phone: {{vm.basic.phone | telFilter }}Email: {{vm.basic.email}}
Billing Note:{{vm.basic.billingNote}}Other Note:{{vm.basic.otherNote}}
32 | 33 |
34 | 35 | 36 | 38 | 39 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
Name: 37 |

A name is required

DOB: 40 |
SSN: 44 |

9 digits are required for SSN

Martial Status:
Gender: 49 | 52 | Address:
City: Postal:
State: Country:
Phone: Email:
Billing Note: Other Note:
72 | 73 | 75 | 79 |
80 | 81 | 85 |
86 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/basic/basic.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient.demographics.basic', []); 6 | })(); 7 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/contact/contact.info.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient.demographics.contact') 6 | .controller('ContactInfoController', ContactInfoController); 7 | 8 | ContactInfoController.$inject = ['$log', '$scope', 'PatientService']; 9 | 10 | function ContactInfoController($log, $scope, PatientService) { 11 | var logger = $log.getInstance('ContactInfoDirective'); 12 | var vm = this; 13 | 14 | // Used for "edit" -> "cancel" reverts 15 | var cachedDataForEditMode = null; 16 | 17 | vm.emailPattern = '.*@.*'; 18 | 19 | vm.mode = null; 20 | 21 | vm.MODES = { 22 | READ: 'read', 23 | SAVE: 'save', 24 | EDIT: 'edit', 25 | CANCEL: 'cancel', 26 | DELETE: 'delete' 27 | }; 28 | 29 | vm.inCreateMode = function() { 30 | // `.isBeingAdded` is the prop applied in the service layer. 31 | // It's existance informs us that this is a new object. 32 | return vm.contact.isBeingAdded; 33 | }; 34 | 35 | vm.inEditMode = function() { 36 | return vm.mode === vm.MODES.EDIT; 37 | }; 38 | 39 | vm.inReadMode = function() { 40 | return vm.mode === vm.MODES.READ; 41 | }; 42 | 43 | vm.changeMode = function(newMode) { 44 | logger.debug('Requesting mode be changed to ' + newMode); 45 | switch(newMode) { 46 | case vm.MODES.CANCEL: 47 | handleCancelMode(); 48 | break; 49 | case vm.MODES.EDIT: 50 | handleEditMode(); 51 | break; 52 | case vm.MODES.SAVE: 53 | handleSaveMode(); 54 | break; 55 | case vm.MODES.DELETE: 56 | handleDeleteMode(); 57 | break; 58 | default: 59 | vm.mode = vm.MODES.READ; 60 | } 61 | }; 62 | 63 | function handleEditMode() { 64 | logger.debug('Caching previous contact state'); 65 | cachedDataForEditMode = angular.copy(vm.contact); 66 | vm.mode = vm.MODES.EDIT; 67 | } 68 | 69 | function handleCancelMode() { 70 | logger.debug('Applying previous contact state cache'); 71 | angular.copy(cachedDataForEditMode, vm.contact); 72 | vm.mode = vm.MODES.READ; 73 | } 74 | 75 | function handleSaveMode() { 76 | vm.contact.isBeingAdded = false; 77 | vm.mode = vm.MODES.READ; 78 | 79 | logger.debug('Releasing previous contact state cache'); 80 | cachedDataForEditMode = null; 81 | 82 | // In a real app, the `PatientService.upsertContact` would be called 83 | } 84 | 85 | function handleDeleteMode() { 86 | if (window.confirm('Are you sure you want to delete this contact?')) { 87 | PatientService.deleteContact(vm.contact); 88 | } 89 | } 90 | 91 | function construct() { 92 | logger.debug('Constructing contacts directive'); 93 | 94 | vm.changeMode(vm.MODES.READ); // Default mode 95 | } 96 | 97 | // vm.contact is not immediately available. Wait for it to be passed. 98 | // why is this variable called unregister????? - DE 99 | var unregister = $scope.$watch(angular.bind(vm, function() { 100 | return this.contact; 101 | }), function (newVal) { 102 | if (newVal) { 103 | unregister(); 104 | construct(); 105 | } 106 | }); 107 | } 108 | })(); 109 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/contact/contact.info.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient.demographics.contact') 6 | .directive('contactInfo', function() { 7 | return { 8 | templateUrl: 'scripts/patient/demographics/contact/contact.info.view.html', 9 | restrict: 'E', 10 | link: function() {}, 11 | scope: { 12 | 'contact': '=' 13 | }, 14 | controllerAs: 'vm', 15 | bindToController: true, 16 | controller: 'ContactInfoController' 17 | }; 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/contact/contact.info.view.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
Name: {{vm.contact.name}}Relation: {{vm.contact.relation}}
Address: {{vm.contact.address}}Phone: {{vm.contact.phone | telFilter}}
City: {{vm.contact.city}}Postal: {{vm.contact.postal}}
State: {{vm.contact.state}}Country: {{vm.contact.country}}
Email: {{vm.contact.email}}
25 | 26 |
27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
Name: 30 |

A name is required

Relation:
Address: Phone: 36 |

A valid phone number is required

City: Postal:
State: Country:
Email:
51 | 52 | 56 | 60 |
61 | 62 | 66 | 70 |
71 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/contact/contact.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient.demographics.contact', []); 6 | })(); 7 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/demographics.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient.demographics') 6 | .controller('DemographicsController', DemographicsController); 7 | 8 | DemographicsController.$inject = ['$log', '$scope', 'PatientService']; 9 | 10 | function DemographicsController($log, $scope, PatientService) { 11 | var logger = $log.getInstance('DemographicsController'); 12 | var vm = this; 13 | 14 | vm.tab = null; 15 | 16 | // Used for if the "Loading..." text blurb needs to be shown 17 | // while the patient context is being set/fetched on the service. 18 | vm.isLoading = null; 19 | 20 | vm.TABS = { 21 | BASIC: 'basic', 22 | CONTACTS: 'contacts' 23 | }; 24 | 25 | vm.inBasicTab = function() { 26 | return vm.tab === vm.TABS.BASIC; 27 | }; 28 | 29 | vm.inContactsTab = function() { 30 | return vm.tab === vm.TABS.CONTACTS; 31 | }; 32 | 33 | vm.changeTab = function(newTab) { 34 | logger.debug('Setting tab to ' + newTab); 35 | vm.tab = newTab; 36 | }; 37 | 38 | vm.addNewContact = function() { 39 | // The service is responsible for adding a fresh object 40 | // with a `isBeingAdded` property. This only applies to the 41 | // `CONTACTS` tab. 42 | PatientService.startAddingNewContact(vm.patientId); 43 | }; 44 | 45 | vm.mockedTab = function() { 46 | alert('This tab is just here for completeness. The real tabs are basic and contacts'); 47 | }; 48 | 49 | function getData() { 50 | vm.isLoading = true; 51 | logger.debug('Retrieving basic patient data'); 52 | PatientService.getPatientData() 53 | .then(function(data) { 54 | vm.basicInformation = data; 55 | 56 | logger.debug('Retrieving patient contact list'); 57 | return PatientService.getPatientContacts(); 58 | }) 59 | .then(function(data) { 60 | vm.contacts = data; 61 | }) 62 | .catch(function(err) { 63 | alert(err); 64 | logger.warn(err); 65 | }) 66 | .finally(function() { 67 | vm.isLoading = false; 68 | }); 69 | } 70 | 71 | function construct() { 72 | logger.debug('Constructing demographics directive'); 73 | 74 | vm.changeTab(vm.TABS.BASIC); // Default tab 75 | 76 | // The demographics directive takes in a patient id. This comes 77 | // from the route `/patient/:id` itself. The point of the `$watch` 78 | // is to make sure the value is 79 | // loaded in before attempting to 80 | // set/fetch the patient context. It should take less than 500ms. 81 | $scope.$watch(angular.bind(vm, function() { 82 | return this.patientId; 83 | }), function (newVal) { 84 | if (newVal) { // a real patient id from the route at this point 85 | logger.info('Retrieved patient id of ' + newVal + ', loading demographics'); 86 | PatientService.setPatientInContext(newVal) 87 | .then(function() { 88 | getData(); 89 | }) 90 | .catch(function(err) { 91 | alert(err); 92 | logger.warn(err); 93 | }); 94 | } 95 | }); 96 | } 97 | 98 | construct(); 99 | } 100 | })(); 101 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/demographics.directive.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient.demographics') 6 | .directive('demographics', function() { 7 | return { 8 | templateUrl: 'scripts/patient/demographics/demographics.view.html', 9 | restrict: 'E', 10 | link: function() {}, 11 | scope: { 12 | patientId: '=' 13 | }, 14 | controllerAs: 'vm', 15 | bindToController: true, 16 | controller: 'DemographicsController' 17 | }; 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/demographics.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient.demographics', [ 6 | 'patientDemographicsExampleApp.patient.demographics.basic', 7 | 'patientDemographicsExampleApp.patient.demographics.contact' 8 | ]); 9 | })(); 10 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/demographics/demographics.view.html: -------------------------------------------------------------------------------- 1 |

Loading...

2 | 3 |
4 | 18 | 19 |
20 | 22 | 23 | 24 |
25 |
26 | 27 | 28 |

29 |
30 | 31 | 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/patient.controller.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient') 6 | .controller('PatientController', PatientController); 7 | 8 | PatientController.$inject = ['$routeParams']; 9 | 10 | function PatientController($routeParams) { 11 | var vm = this; 12 | 13 | // The patient id comes from the route `/patient/:id` 14 | vm.patientId = $routeParams.pid; 15 | } 16 | })(); 17 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/patient.html: -------------------------------------------------------------------------------- 1 |

Patient Overview

2 | 3 | 4 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/patient.module.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient', [ 6 | 'patientDemographicsExampleApp.patient.demographics' 7 | ]) 8 | .config(function ($routeProvider) { 9 | $routeProvider 10 | .when('/patient/:pid', { 11 | templateUrl: 'scripts/patient/patient.html', 12 | controller: 'PatientController', 13 | controllerAs: 'vm' 14 | }) 15 | .otherwise({ 16 | redirectTo: '/patient/1337' 17 | }); 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/scripts/patient/patient.service.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('patientDemographicsExampleApp.patient') 6 | .factory('PatientService', PatientService); 7 | 8 | PatientService.$inject = ['$log', '$q']; 9 | 10 | function PatientService($log, $q) { 11 | var logger = $log.getInstance('PatientService'); 12 | 13 | // The reason this service is only concerned with 1 patient context 14 | // at a time is because EMR (electronic medical records) workflows 15 | // tend to work with 1 patient at a time. For instance, a doctor at 16 | // small facility may "pull" up patient with id 1337 to have an 17 | // encounter (patient visit) where the following actions happen: 18 | // 19 | // 1) Patient change their address, so "basic" demographics are updated. 20 | // 2) Doctor notes that the patient has a new medical issue and adds it 21 | // to their record using the "medical issues" module. 22 | // 3) Doctor prescribes a new medication using the "pharmacy orders" module. 23 | // 4) Doctor is alerted (pop up message from clinical decision support module) 24 | // that patient needs to have a particular screening soon. 25 | // 26 | // Ddespite the doctor switching around modules in the application, this 1 27 | // patient is in "context". As you can imagine, It is important to know that 28 | // the data being entered/edited/review from these various modules is tied to 29 | // a particular patient. 30 | var patientIdInContext = null; 31 | 32 | // Dummy data for patient because we don't have a server. 33 | var testData = { 34 | 1337: { 35 | basic: { 36 | name: 'John Doe', 37 | dob: '1990-11-04', 38 | ss: 999999999, 39 | martialStatus: 'Single', 40 | gender: 'Male', 41 | billingNote: ' N/A', 42 | otherNote: ' N/A', 43 | address: '321 Bazbop Ln', 44 | city: 'CoolCity', 45 | postal: 54321, 46 | state: 'Texas', 47 | country: 'US', 48 | phone: 1234567899, 49 | email: 'foo@bar.com' 50 | }, 51 | contacts: [{ 52 | name: 'Jane Doe', 53 | relation: 'Mother', 54 | address: '123 Foobar Ln', 55 | city: 'CoolCity', 56 | postal: 12345, 57 | state: 'Texas', 58 | country: 'US', 59 | phone: 1234567899, 60 | email: 'bar@foo.com' 61 | }, { 62 | name: 'Sam Doe', 63 | relation: 'Father', 64 | address: '123 Foobar Ln', 65 | city: 'CoolCity', 66 | postal: 12345, 67 | state: 'Texas', 68 | country: 'US', 69 | phone: 9876543211, 70 | email: 'baz@bop.com' 71 | }] 72 | } 73 | }; 74 | 75 | // This needs to be called prior to any other call in this service. 76 | function setPatientInContext(patientId) { 77 | var deferred = $q.defer(); 78 | 79 | logger.info('attempting to set patient context to patient ' + patientId); 80 | 81 | var res = testData[patientId]; 82 | if (!res) { 83 | logger.warn('patient ' + patientId + ' doesn\'t exist'); 84 | deferred.reject('patient doesn\'t exist'); 85 | } else { 86 | patientIdInContext = patientId; 87 | deferred.resolve(); 88 | } 89 | 90 | return deferred.promise; 91 | } 92 | 93 | function getPatientInContext() { 94 | return patientIdInContext; 95 | } 96 | 97 | // Basic data. 98 | function getPatientData() { 99 | logger.debug('retrieving patient basic data'); 100 | 101 | return $q(function(resolve) { 102 | return resolve(testData[patientIdInContext].basic); 103 | }); 104 | } 105 | 106 | // Contacts data. 107 | function getPatientContacts() { 108 | logger.debug('retrieving patient contacts data'); 109 | return $q(function(resolve) { 110 | return resolve(testData[patientIdInContext].contacts); 111 | }); 112 | } 113 | 114 | function deleteContact(contact) { 115 | logger.info('deleting contact "' + contact.relation + '"'); 116 | return $q(function(resolve) { 117 | var contacts = testData[patientIdInContext].contacts; 118 | var matchIndex; 119 | contacts.some(function(element, index) { 120 | if (element.name === contact.name) { 121 | matchIndex = index; 122 | return true; 123 | } 124 | }); 125 | 126 | if (matchIndex > -1) { 127 | contacts.splice(matchIndex, 1); 128 | } 129 | 130 | return resolve(); 131 | }); 132 | } 133 | 134 | // Adds a fresh object for a contact to be filled in. 135 | function startAddingNewContact() { 136 | logger.debug('Adding a fresh contact entry to be filled in'); 137 | return $q(function(resolve) { 138 | testData[patientIdInContext].contacts.push({isBeingAdded: true}); 139 | return resolve(); 140 | }); 141 | } 142 | 143 | return { 144 | setPatientInContext: setPatientInContext, 145 | getPatientInContext: getPatientInContext, 146 | getPatientData: getPatientData, 147 | getPatientContacts: getPatientContacts, 148 | deleteContact: deleteContact, 149 | startAddingNewContact: startAddingNewContact, 150 | clear: function() { /* mocked out as an example :) */ } 151 | }; 152 | } 153 | })(); 154 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/app/styles/main.css: -------------------------------------------------------------------------------- 1 | /** TODO: split up CSS into the modules... tweak grunt to do this */ 2 | 3 | .basic-info-form input.ng-invalid.ng-touched, 4 | .contact-info-form input.ng-invalid.ng-touched { 5 | background-color: #FA787E; 6 | } 7 | 8 | 9 | .help-block { 10 | color: red; 11 | border: red solid 1px; 12 | display: inline-block; 13 | padding: 0 5px; 14 | margin-bottom: 5px; 15 | } 16 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patient-demographics-example", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "angular": "^1.4.0", 6 | "bootstrap": "^3.2.0", 7 | "angular-animate": "^1.4.0", 8 | "angular-cookies": "^1.4.0", 9 | "angular-resource": "^1.4.0", 10 | "angular-route": "^1.4.0", 11 | "angular-sanitize": "^1.4.0", 12 | "angular-touch": "^1.4.0", 13 | "angular-logger": "^1.4.0", 14 | "moment": "^2.17.1", 15 | "angularjs-datepicker": "^2.1.9", 16 | "angular-ui-mask": "^1.8.7" 17 | }, 18 | "devDependencies": { 19 | "angular-mocks": "^1.4.0" 20 | }, 21 | "appPath": "app", 22 | "moduleName": "patientDemographicsExampleApp", 23 | "overrides": { 24 | "bootstrap": { 25 | "main": [ 26 | "less/bootstrap.less", 27 | "dist/css/bootstrap.css", 28 | "dist/js/bootstrap.js" 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patientdemographicsexample", 3 | "private": true, 4 | "devDependencies": { 5 | "autoprefixer-core": "^5.2.1", 6 | "grunt": "^0.4.5", 7 | "grunt-angular-templates": "^0.5.7", 8 | "grunt-concurrent": "^1.0.0", 9 | "grunt-contrib-clean": "^0.6.0", 10 | "grunt-contrib-concat": "^0.5.0", 11 | "grunt-contrib-connect": "^0.9.0", 12 | "grunt-contrib-copy": "^0.7.0", 13 | "grunt-contrib-cssmin": "^1.0.2", 14 | "grunt-contrib-htmlmin": "^0.4.0", 15 | "grunt-contrib-imagemin": "^1.0.0", 16 | "grunt-contrib-jshint": "^0.11.0", 17 | "grunt-contrib-uglify": "^0.7.0", 18 | "grunt-contrib-watch": "^0.6.1", 19 | "grunt-filerev": "^2.1.2", 20 | "grunt-google-cdn": "^0.4.3", 21 | "grunt-jscs": "^1.8.0", 22 | "grunt-karma": "^2.0.0", 23 | "grunt-newer": "^1.1.0", 24 | "grunt-ng-annotate": "^0.9.2", 25 | "grunt-postcss": "^0.5.5", 26 | "grunt-svgmin": "^2.0.0", 27 | "grunt-usemin": "^3.0.0", 28 | "grunt-wiredep": "^2.0.0", 29 | "jasmine-core": "^2.5.2", 30 | "jit-grunt": "^0.9.1", 31 | "jshint-stylish": "^1.0.0", 32 | "karma": "^1.3.0", 33 | "karma-coverage": "^1.1.1", 34 | "karma-jasmine": "^1.1.0", 35 | "karma-phantomjs-launcher": "^1.0.2", 36 | "phantomjs-prebuilt": "^2.1.14", 37 | "time-grunt": "^1.0.0" 38 | }, 39 | "engines": { 40 | "node": ">=0.10.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "esnext": true, 7 | "jasmine": true, 8 | "latedef": true, 9 | "noarg": true, 10 | "node": true, 11 | "strict": true, 12 | "undef": true, 13 | "unused": true, 14 | "globals": { 15 | "angular": false, 16 | "inject": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on 2016-12-27 3 | 4 | module.exports = function(config) { 5 | 'use strict'; 6 | 7 | config.set({ 8 | // enable / disable watching file and executing tests whenever any file changes 9 | autoWatch: true, 10 | 11 | // base path, that will be used to resolve files and exclude 12 | basePath: '../', 13 | 14 | // testing framework to use (jasmine/mocha/qunit/...) 15 | // as well as any additional frameworks (requirejs/chai/sinon/...) 16 | frameworks: [ 17 | 'jasmine' 18 | ], 19 | 20 | // list of files / patterns to load in the browser 21 | files: [ 22 | // bower:js 23 | 'bower_components/jquery/dist/jquery.js', 24 | 'bower_components/angular/angular.js', 25 | 'bower_components/bootstrap/dist/js/bootstrap.js', 26 | 'bower_components/angular-animate/angular-animate.js', 27 | 'bower_components/angular-cookies/angular-cookies.js', 28 | 'bower_components/angular-resource/angular-resource.js', 29 | 'bower_components/angular-route/angular-route.js', 30 | 'bower_components/angular-sanitize/angular-sanitize.js', 31 | 'bower_components/angular-touch/angular-touch.js', 32 | 'bower_components/angular-logger/dist/angular-logger.js', 33 | 'bower_components/moment/moment.js', 34 | 'bower_components/angularjs-datepicker/dist/angular-datepicker.js', 35 | 'bower_components/angular-ui-mask/dist/mask.js', 36 | 'bower_components/angular-mocks/angular-mocks.js', 37 | // endbower 38 | 'app/scripts/**/*.module.js', 39 | 'app/scripts/**/*.js', 40 | 'test/mock/**/*.js', 41 | 'test/spec/**/*.js' 42 | ], 43 | 44 | // list of files / patterns to exclude 45 | exclude: [ 46 | ], 47 | 48 | // web server port 49 | port: 8080, 50 | 51 | // Start these browsers, currently available: 52 | // - Chrome 53 | // - ChromeCanary 54 | // - Firefox 55 | // - Opera 56 | // - Safari (only Mac) 57 | // - PhantomJS 58 | // - IE (only Windows) 59 | browsers: [ 60 | 'PhantomJS' 61 | ], 62 | 63 | reporters: ['progress', 'coverage'], 64 | 65 | preprocessors: { 66 | 'app/scripts/**/*.js': ['coverage'] 67 | }, 68 | 69 | coverageReporter: { 70 | type: 'html', 71 | dir: 'coverage' 72 | }, 73 | 74 | // Which plugins to enable 75 | plugins: [ 76 | 'karma-phantomjs-launcher', 77 | 'karma-jasmine', 78 | 'karma-coverage' 79 | ], 80 | 81 | // Continuous Integration mode 82 | // if true, it capture browsers, run tests and exit 83 | singleRun: false, 84 | 85 | colors: true, 86 | 87 | // level of logging 88 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 89 | logLevel: config.LOG_INFO, 90 | 91 | // Uncomment the following lines if you are using grunt's server to run the tests 92 | // proxies: { 93 | // '/': 'http://localhost:9000/' 94 | // }, 95 | // URL root prevent conflicts with the site root 96 | // urlRoot: '_karma_' 97 | }); 98 | }; 99 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/test/spec/patient/demographics/basic/basic.info.controller.spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | describe('BasicInfoController', function () { 5 | var controller; 6 | var scope; 7 | 8 | var testData = { 9 | name: 'John Doe', 10 | dob: '1990-11-04', 11 | ss: 999999999, 12 | martialStatus: 'Single', 13 | gender: 'Male', 14 | billingNote: ' N/A', 15 | otherNote: ' N/A', 16 | address: '321 Bazbop Ln', 17 | city: 'CoolCity', 18 | postal: 54321, 19 | state: 'Texas', 20 | country: 'US', 21 | phone: 321321431, 22 | email: 'foo@bar.com' 23 | }; 24 | 25 | beforeEach(module('patientDemographicsExampleApp')); 26 | 27 | beforeEach(inject(function($controller, $rootScope, $log) { 28 | scope = $rootScope.$new(); 29 | 30 | controller = $controller('BasicInfoController', { 31 | $log: $log, 32 | $scope: scope 33 | }); 34 | 35 | controller.basic = testData; 36 | scope.$digest(); 37 | })); 38 | 39 | describe('construct', function() { 40 | it('sets the default mode to read mode', function() { 41 | expect(controller.mode).toBe(controller.MODES.READ); 42 | }); 43 | 44 | it('caches the initial date for the date picker', function() { 45 | expect(controller.initialDate).toBe('1990-11-04'); 46 | }); 47 | }); 48 | 49 | describe('changeMode', function() { 50 | describe('edit mode', function() { 51 | beforeEach(function() { 52 | spyOn(angular, 'copy').and.callThrough(); 53 | controller.changeMode(controller.MODES.EDIT); 54 | }); 55 | 56 | it('caches the current data in case of cancellation', function() { 57 | expect(angular.copy).toHaveBeenCalledWith(testData); 58 | }); 59 | 60 | it('sets the mode to edit mode', function() { 61 | expect(controller.mode).toBe(controller.MODES.EDIT); 62 | }); 63 | }); 64 | 65 | describe('cancel mode', function() { 66 | beforeEach(function() { 67 | spyOn(angular, 'copy').and.callThrough(); 68 | controller.changeMode('edit'); 69 | controller.name = 'Not John Doe'; 70 | controller.changeMode(controller.MODES.CANCEL); 71 | }); 72 | 73 | it('reverts current data to the cache (original data, in this case)', function() { 74 | expect(angular.copy).toHaveBeenCalledWith(testData, jasmine.any(Object)); 75 | }); 76 | 77 | it('uses the initial data cached before editing', function() { 78 | // Examines the value that we know was touched (simple diff check) 79 | expect(controller.basic.name).toBe('John Doe'); 80 | }); 81 | 82 | it('sets the mode to read mode (functionally equiv. to cancel mode)', function() { 83 | expect(controller.mode).toBe(controller.MODES.READ); 84 | }); 85 | }); 86 | 87 | describe('save mode', function() { 88 | beforeEach(function() { 89 | spyOn(angular, 'copy').and.callThrough(); 90 | controller.changeMode('edit'); 91 | controller.basic.name = 'Foobar John Doe'; 92 | controller.changeMode(controller.MODES.SAVE); 93 | }); 94 | 95 | it('uses the name data save after editing', function() { 96 | // Examines the value that we know was touched (simple diff check) 97 | expect(controller.basic.name).toBe('Foobar John Doe'); 98 | }); 99 | 100 | it('sets the mode to read mode (functionally equiv. to save mode)', function() { 101 | expect(controller.mode).toBe(controller.MODES.READ); 102 | }); 103 | }); 104 | }); 105 | 106 | describe('inEditMode', function() { 107 | describe('positive case', function() { 108 | beforeEach(function() { 109 | controller.changeMode(controller.MODES.EDIT); 110 | }); 111 | 112 | it('evaluates to true', function() { 113 | expect(controller.inEditMode()).toBe(true); 114 | }); 115 | }); 116 | 117 | describe('negative case', function() { 118 | beforeEach(function() { 119 | controller.changeMode(controller.MODES.READ); 120 | }); 121 | 122 | it('evaluates to false', function() { 123 | expect(controller.inEditMode()).toBe(false); 124 | }); 125 | }); 126 | }); 127 | 128 | describe('inReadMode', function() { 129 | describe('positive case - save', function() { 130 | beforeEach(function() { 131 | controller.changeMode(controller.MODES.SAVE); 132 | }); 133 | 134 | it('evaluates to true', function() { 135 | expect(controller.inReadMode()).toBe(true); 136 | }); 137 | }); 138 | 139 | describe('positive case - cancel', function() { 140 | beforeEach(function() { 141 | controller.changeMode(controller.MODES.CANCEL); 142 | }); 143 | 144 | it('evaluates to true', function() { 145 | expect(controller.inReadMode()).toBe(true); 146 | }); 147 | }); 148 | 149 | describe('positive case - read', function() { 150 | beforeEach(function() { 151 | controller.changeMode(controller.MODES.READ); 152 | }); 153 | 154 | it('evaluates to true', function() { 155 | expect(controller.inReadMode()).toBe(true); 156 | }); 157 | }); 158 | 159 | describe('negative case', function() { 160 | beforeEach(function() { 161 | controller.changeMode(controller.MODES.EDIT); 162 | }); 163 | 164 | it('evaluates to false', function() { 165 | expect(controller.inReadMode()).toBe(false); 166 | }); 167 | }); 168 | }); 169 | }); 170 | })(); 171 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/test/spec/patient/demographics/contact/contact.info.controller.spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | describe('ContactInfoController', function () { 5 | var controller; 6 | var scope; 7 | 8 | var testData = { 9 | name: 'Sam Doe', 10 | relation: 'Father', 11 | address: '123 Foobar Ln', 12 | city: 'CoolCity', 13 | postal: 12345, 14 | state: 'Texas', 15 | country: 'US', 16 | phone: 9876543211, 17 | email: 'baz@bop.com' 18 | }; 19 | 20 | beforeEach(module('patientDemographicsExampleApp')); 21 | 22 | beforeEach(inject(function($controller, $rootScope, $log, PatientService) { 23 | scope = $rootScope.$new(); 24 | 25 | controller = $controller('ContactInfoController', { 26 | $log: $log, 27 | $scope: scope, 28 | PatientService: PatientService 29 | }); 30 | 31 | controller.contact = testData; 32 | scope.$digest(); 33 | })); 34 | 35 | describe('construct', function() { 36 | it('sets the default mode to read mode', function() { 37 | expect(controller.mode).toBe(controller.MODES.READ); 38 | }); 39 | }); 40 | 41 | describe('changeMode', function() { 42 | describe('edit mode', function() { 43 | beforeEach(function() { 44 | // what does callThrough really do? - Personal Research Q 45 | spyOn(angular, 'copy').and.callThrough(); 46 | controller.changeMode(controller.MODES.EDIT); 47 | }); 48 | 49 | it('caches the current data in case of cancellation', function() { 50 | expect(angular.copy).toHaveBeenCalledWith(testData); 51 | }); 52 | 53 | it('sets the mode to edit mode', function() { 54 | expect(controller.mode).toBe(controller.MODES.EDIT); 55 | }); 56 | }); 57 | 58 | describe('cancel mode', function() { 59 | beforeEach(function() { 60 | spyOn(angular, 'copy').and.callThrough(); 61 | controller.changeMode('edit'); 62 | controller.name = 'Not Sam Doe'; 63 | controller.changeMode(controller.MODES.CANCEL); 64 | }); 65 | 66 | // What does jasmine.any() really do? - Personal Research Q 67 | it('reverts current data to the cache (original data, in this case)', function() { 68 | expect(angular.copy).toHaveBeenCalledWith(testData, jasmine.any(Object)); 69 | }); 70 | 71 | it('uses the initial data cached before editing', function() { 72 | // Examines the value that we know was touched (simple diff check) 73 | expect(controller.contact.name).toBe('Sam Doe'); 74 | }); 75 | 76 | it('sets the mode to read mode (functionally equiv. to cancel mode)', function() { 77 | expect(controller.mode).toBe(controller.MODES.READ); 78 | }); 79 | }); 80 | 81 | describe('save mode', function() { 82 | beforeEach(function() { 83 | // why do you even need a spy here? - DE 84 | spyOn(angular, 'copy').and.callThrough(); 85 | controller.changeMode('edit'); 86 | controller.contact.name = 'CopyMode Sam Doe'; 87 | controller.changeMode(controller.MODES.SAVE); 88 | }); 89 | 90 | it('uses the name data save after editing', function() { 91 | // Examines the value that we know was touched (simple diff check) 92 | expect(controller.contact.name).toBe('CopyMode Sam Doe'); 93 | }); 94 | 95 | it('sets the mode to read mode (functionally equiv. to save mode)', function() { 96 | expect(controller.mode).toBe(controller.MODES.READ); 97 | }); 98 | }); 99 | }); 100 | 101 | describe('inEditMode', function() { 102 | describe('positive case', function() { 103 | beforeEach(function() { 104 | controller.changeMode(controller.MODES.EDIT); 105 | }); 106 | 107 | it('evaluates to true', function() { 108 | expect(controller.inEditMode()).toBe(true); 109 | }); 110 | }); 111 | 112 | describe('negative case', function() { 113 | beforeEach(function() { 114 | controller.changeMode(controller.MODES.READ); 115 | }); 116 | 117 | it('evaluates to false', function() { 118 | expect(controller.inEditMode()).toBe(false); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('inReadMode', function() { 124 | describe('positive case - save', function() { 125 | beforeEach(function() { 126 | controller.changeMode(controller.MODES.SAVE); 127 | }); 128 | 129 | it('evaluates to true', function() { 130 | expect(controller.inReadMode()).toBe(true); 131 | }); 132 | }); 133 | 134 | describe('positive case - cancel', function() { 135 | beforeEach(function() { 136 | controller.changeMode(controller.MODES.CANCEL); 137 | }); 138 | 139 | it('evaluates to true', function() { 140 | expect(controller.inReadMode()).toBe(true); 141 | }); 142 | }); 143 | 144 | describe('positive case - read', function() { 145 | beforeEach(function() { 146 | controller.changeMode(controller.MODES.READ); 147 | }); 148 | 149 | it('evaluates to true', function() { 150 | expect(controller.inReadMode()).toBe(true); 151 | }); 152 | }); 153 | 154 | describe('negative case', function() { 155 | beforeEach(function() { 156 | controller.changeMode(controller.MODES.EDIT); 157 | }); 158 | 159 | it('evaluates to false', function() { 160 | expect(controller.inReadMode()).toBe(false); 161 | }); 162 | }); 163 | }); 164 | }); 165 | })(); 166 | -------------------------------------------------------------------------------- /samples/angular-patient-demographics-example/test/spec/patient/patient.service.spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | describe('PatientService', function () { 5 | var patientService; 6 | var testPatientId = 1337; 7 | var rootScope; 8 | 9 | beforeEach(module('patientDemographicsExampleApp')); 10 | 11 | beforeEach(inject(function(_PatientService_, $rootScope) { 12 | patientService = _PatientService_; 13 | rootScope = $rootScope; 14 | })); 15 | 16 | describe('setPatientInContext', function() { 17 | describe('valid patient', function() { 18 | beforeEach(function() { 19 | patientService.setPatientInContext(testPatientId); 20 | }); 21 | 22 | it('is now in context', function() { 23 | expect(patientService.getPatientInContext()).toBe(testPatientId); 24 | }); 25 | }); 26 | 27 | describe('invalid patient', function() { 28 | beforeEach(function() { 29 | patientService.setPatientInContext(9001); 30 | }); 31 | 32 | it('is not in context', function() { 33 | expect(patientService.getPatientInContext()).toBe(null); 34 | }); 35 | }); 36 | }); 37 | 38 | describe('getPatientData', function() { 39 | beforeEach(function() { 40 | patientService.setPatientInContext(testPatientId); 41 | }); 42 | 43 | it('returns the correct basic structure', function(done) { 44 | patientService.getPatientData() 45 | .then(function(res) { 46 | var returnedKeys = Object.keys(res).sort(); 47 | 48 | var expectedKeys = [ 49 | 'name', 50 | 'dob', 51 | 'ss', 52 | 'martialStatus', 53 | 'gender', 54 | 'billingNote', 55 | 'otherNote', 56 | 'address', 57 | 'city', 58 | 'postal', 59 | 'state', 60 | 'country', 61 | 'phone', 62 | 'email' 63 | ].sort(); 64 | 65 | expect(returnedKeys).toEqual(expectedKeys); 66 | done(); 67 | }); 68 | rootScope.$digest(); 69 | }); 70 | }); 71 | 72 | describe('getPatientContacts', function() { 73 | beforeEach(function() { 74 | patientService.setPatientInContext(testPatientId); 75 | }); 76 | 77 | it('is a list of contacts', function(done) { 78 | patientService.getPatientContacts() 79 | .then(function(res) { 80 | expect(res.length).toBe(2); 81 | done(); 82 | }); 83 | rootScope.$digest(); 84 | }); 85 | 86 | it('returns the correct contacts structure', function(done) { 87 | patientService.getPatientContacts() 88 | .then(function(res) { 89 | var returnedKeys = Object.keys(res[0]).sort(); 90 | 91 | var expectedKeys = [ 92 | 'name', 93 | 'relation', 94 | 'address', 95 | 'city', 96 | 'postal', 97 | 'state', 98 | 'country', 99 | 'phone', 100 | 'email' 101 | ].sort(); 102 | 103 | expect(returnedKeys).toEqual(expectedKeys); 104 | done(); 105 | }); 106 | rootScope.$digest(); 107 | }); 108 | }); 109 | 110 | describe('startAddingNewContact', function() { 111 | beforeEach(function() { 112 | patientService.setPatientInContext(testPatientId); 113 | }); 114 | 115 | it('pushes a fresh object to the contact list', function(done) { 116 | patientService.startAddingNewContact() 117 | .then(function() { 118 | return patientService.getPatientContacts(); 119 | }) 120 | .then(function(res) { 121 | var match = res.filter(function(contact) { 122 | return contact.isBeingAdded === true; 123 | }); 124 | 125 | expect(match).toEqual([{isBeingAdded: true}]); 126 | done(); 127 | }); 128 | rootScope.$digest(); 129 | }); 130 | }); 131 | 132 | describe('deleteContact', function() { 133 | describe('when the contact to be deleted exists', function() { 134 | var testContact = { 135 | name: 'Jane Doe', 136 | relation: 'Mother', 137 | address: '123 Foobar Ln', 138 | city: 'CoolCity', 139 | postal: 12345, 140 | state: 'Texas', 141 | country: 'US', 142 | phone: 1231231234, 143 | email: 'bar@foo.com' 144 | }; 145 | 146 | beforeEach(function() { 147 | patientService.setPatientInContext(testPatientId); 148 | }); 149 | 150 | it('removes the contact object from the list', function(done) { 151 | patientService.deleteContact(testContact) 152 | .then(function() { 153 | return patientService.getPatientContacts(); 154 | }) 155 | .then(function(res) { 156 | var match = res.filter(function(contact) { 157 | return contact.name === testContact.name; 158 | }); 159 | 160 | expect(match).toEqual([]); 161 | done(); 162 | }); 163 | rootScope.$digest(); 164 | }); 165 | }); 166 | 167 | describe('when the contact to be deleted doesn\'t exist', function() { 168 | var testContact = { 169 | name: 'I Don\'t Exist :(', 170 | relation: 'Nobody', 171 | address: 'Nowhere', 172 | city: 'Notown', 173 | postal: 11111, 174 | state: 'stateless', 175 | country: 'NA', 176 | phone: 1111111111, 177 | email: 'null@nil.com' 178 | }; 179 | 180 | beforeEach(function() { 181 | patientService.setPatientInContext(testPatientId); 182 | }); 183 | 184 | it('no contacts are removed from the list', function(done) { 185 | patientService.deleteContact(testContact) 186 | .then(function() { 187 | return patientService.getPatientContacts(); 188 | }) 189 | .then(function(res) { 190 | expect(res.length).toBe(2); 191 | done(); 192 | }); 193 | rootScope.$digest(); 194 | }); 195 | }); 196 | }); 197 | }); 198 | })(); 199 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/.eslintignore: -------------------------------------------------------------------------------- 1 | blueprints/**/files/** 2 | coverage/** 3 | node_modules/** 4 | dist/** 5 | src/index.html 6 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "plugins": [ 8 | "babel", 9 | "react", 10 | "promise" 11 | ], 12 | "env": { 13 | "browser" : true 14 | }, 15 | "globals": { 16 | "__DEV__" : false, 17 | "__TEST__" : false, 18 | "__PROD__" : false, 19 | "__COVERAGE__" : false 20 | }, 21 | "rules": { 22 | "key-spacing" : 0, 23 | "semi" : [0, "always"], 24 | "space-before-function-paren" : [0, "always"], 25 | "react/jsx-space-before-closing" : [0, "always"], 26 | "react/prop-types" : [0, "always"], 27 | "object-curly-spacing" : [0, "always"], 28 | "react/jsx-indent" : [0, "always"], 29 | "react/jsx-indent-props" : [0, "always"], 30 | "indent" : [0, "always"], 31 | "jsx-quotes" : [2, "prefer-single"], 32 | "max-len" : [2, 120, 2], 33 | "comma-spacing" : [0, "always"], 34 | "react/jsx-no-bind" : [0, "always"] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.log 3 | 4 | node_modules 5 | 6 | dist 7 | coverage 8 | 9 | .idea/ 10 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | install: 11 | - npm install -g yarn 12 | - yarn install 13 | 14 | script: 15 | - npm run deploy:dev 16 | - npm run deploy:prod 17 | 18 | after_success: 19 | - npm run codecov 20 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/README.md: -------------------------------------------------------------------------------- 1 | # React/Redux Patient Demographics Example Project 2 | 3 | Sample React/Redux app. Adheres to community best practices. 4 | 5 | ## Dev 6 | 7 | Install via `npm install` 8 | 9 | Run via `npm start` 10 | 11 | ## Prod 12 | 13 | Run `npm run lint && npm run test && npm run clean && npm run compile` and check `dist/` 14 | 15 | To see the starter kit's README for further instruction, visit https://github.com/davezuko/react-redux-starter-kit 16 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/bin/compile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const webpack = require('webpack'); 3 | const debug = require('debug')('app:bin:compile'); 4 | const webpackConfig = require('../config/webpack.config'); 5 | const project = require('../config/project.config'); 6 | 7 | // Wrapper around webpack to promisify its compiler and supply friendly logging 8 | const webpackCompiler = (webpackConfig) => 9 | new Promise((resolve, reject) => { 10 | const compiler = webpack(webpackConfig); 11 | 12 | compiler.run((err, stats) => { 13 | if (err) { 14 | debug('Webpack compiler encountered a fatal error.', err); 15 | return reject(err); 16 | } 17 | 18 | const jsonStats = stats.toJson(); 19 | debug('Webpack compile completed.'); 20 | debug(stats.toString(project.compiler_stats)); 21 | 22 | if (jsonStats.errors.length > 0) { 23 | debug('Webpack compiler encountered errors.'); 24 | debug(jsonStats.errors.join('\n')); 25 | return reject(new Error('Webpack compiler encountered errors')); 26 | } else if (jsonStats.warnings.length > 0) { 27 | debug('Webpack compiler encountered warnings.'); 28 | debug(jsonStats.warnings.join('\n')); 29 | } else { 30 | debug('No errors or warnings encountered.'); 31 | } 32 | resolve(jsonStats); 33 | }); 34 | }); 35 | 36 | const compile = () => { 37 | debug('Starting compiler.'); 38 | return Promise.resolve() 39 | .then(() => webpackCompiler(webpackConfig)) 40 | .then(stats => { 41 | if (stats.warnings.length && project.compiler_fail_on_warning) { 42 | throw new Error('Config set to fail on warning, exiting with status code "1".'); 43 | } 44 | debug('Copying static assets to dist folder.'); 45 | fs.copySync(project.paths.public(), project.paths.dist()); 46 | }) 47 | .then(() => { 48 | debug('Compilation completed successfully.'); 49 | }) 50 | .catch((err) => { 51 | debug('Compiler encountered an error.', err); 52 | process.exit(1); 53 | }); 54 | }; 55 | 56 | compile(); 57 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/bin/dev-server.js: -------------------------------------------------------------------------------- 1 | const project = require('../config/project.config'); 2 | const server = require('../server/main'); 3 | const debug = require('debug')('app:bin:dev-server'); 4 | 5 | server.listen(project.server_port); 6 | debug(`Server is now running at http://localhost:${project.server_port}.`); 7 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/config/environments.config.js: -------------------------------------------------------------------------------- 1 | // Here is where you can define configuration overrides based on the execution environment. 2 | // Supply a key to the default export matching the NODE_ENV that you wish to target, and 3 | // the base configuration will apply your overrides before exporting itself. 4 | module.exports = { 5 | // ====================================================== 6 | // Overrides when NODE_ENV === 'development' 7 | // ====================================================== 8 | // NOTE: In development, we use an explicit public path when the assets 9 | // are served webpack by to fix this issue: 10 | // http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts/34133809#34133809 11 | development : (config) => ({ 12 | compiler_public_path : `http://${config.server_host}:${config.server_port}/` 13 | }), 14 | 15 | // ====================================================== 16 | // Overrides when NODE_ENV === 'production' 17 | // ====================================================== 18 | production : (config) => ({ 19 | compiler_public_path : '/', 20 | compiler_fail_on_warning : false, 21 | compiler_hash_type : 'chunkhash', 22 | compiler_devtool : null, 23 | compiler_stats : { 24 | chunks : true, 25 | chunkModules : true, 26 | colors : true 27 | } 28 | }) 29 | }; 30 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/config/karma.config.js: -------------------------------------------------------------------------------- 1 | const argv = require('yargs').argv; 2 | const project = require('./project.config'); 3 | const webpackConfig = require('./webpack.config'); 4 | const debug = require('debug')('app:config:karma'); 5 | 6 | debug('Creating configuration.'); 7 | const karmaConfig = { 8 | basePath : '../', // project root in relation to bin/karma.js 9 | files : [ 10 | { 11 | pattern : `./${project.dir_test}/test-bundler.js`, 12 | watched : false, 13 | served : true, 14 | included : true 15 | } 16 | ], 17 | singleRun : !argv.watch, 18 | frameworks : ['mocha'], 19 | reporters : ['mocha'], 20 | preprocessors : { 21 | [`${project.dir_test}/test-bundler.js`] : ['webpack'] 22 | }, 23 | browsers : ['PhantomJS'], 24 | webpack : { 25 | devtool : 'cheap-module-source-map', 26 | resolve : Object.assign({}, webpackConfig.resolve, { 27 | alias : Object.assign({}, webpackConfig.resolve.alias, { 28 | sinon : 'sinon/pkg/sinon.js' 29 | }) 30 | }), 31 | plugins : webpackConfig.plugins, 32 | module : { 33 | noParse : [ 34 | /\/sinon\.js/ 35 | ], 36 | loaders : webpackConfig.module.loaders.concat([ 37 | { 38 | test : /sinon(\\|\/)pkg(\\|\/)sinon\.js/, 39 | loader : 'imports?define=>false,require=>false' 40 | } 41 | ]) 42 | }, 43 | // Enzyme fix, see: 44 | // https://github.com/airbnb/enzyme/issues/47 45 | externals : Object.assign({}, webpackConfig.externals, { 46 | 'react/addons' : true, 47 | 'react/lib/ExecutionEnvironment' : true, 48 | 'react/lib/ReactContext' : 'window' 49 | }), 50 | sassLoader : webpackConfig.sassLoader 51 | }, 52 | webpackMiddleware : { 53 | noInfo : true 54 | }, 55 | coverageReporter : { 56 | reporters : project.coverage_reporters 57 | } 58 | }; 59 | 60 | if (project.globals.__COVERAGE__) { 61 | karmaConfig.reporters.push('coverage'); 62 | karmaConfig.webpack.module.preLoaders = [{ 63 | test : /\.(js|jsx)$/, 64 | include : new RegExp(project.dir_client), 65 | exclude : /node_modules/, 66 | loader : 'babel', 67 | query : Object.assign({}, project.compiler_babel, { 68 | plugins : (project.compiler_babel.plugins || []).concat('istanbul') 69 | }) 70 | }]; 71 | } 72 | 73 | module.exports = (cfg) => cfg.set(karmaConfig); 74 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/config/project.config.js: -------------------------------------------------------------------------------- 1 | /* eslint key-spacing:0 spaced-comment:0 */ 2 | const path = require('path'); 3 | const debug = require('debug')('app:config:project'); 4 | const argv = require('yargs').argv; 5 | const ip = require('ip'); 6 | 7 | debug('Creating default configuration.'); 8 | // ======================================================== 9 | // Default Configuration 10 | // ======================================================== 11 | const config = { 12 | env : process.env.NODE_ENV || 'development', 13 | 14 | // ---------------------------------- 15 | // Project Structure 16 | // ---------------------------------- 17 | path_base : path.resolve(__dirname, '..'), 18 | dir_client : 'src', 19 | dir_dist : 'dist', 20 | dir_public : 'public', 21 | dir_server : 'server', 22 | dir_test : 'tests', 23 | 24 | // ---------------------------------- 25 | // Server Configuration 26 | // ---------------------------------- 27 | server_host : ip.address(), // use string 'localhost' to prevent exposure on local network 28 | server_port : process.env.PORT || 3000, 29 | 30 | // ---------------------------------- 31 | // Compiler Configuration 32 | // ---------------------------------- 33 | compiler_babel : { 34 | cacheDirectory : true, 35 | plugins : ['transform-runtime'], 36 | presets : ['es2015', 'react', 'stage-0'] 37 | }, 38 | compiler_devtool : 'source-map', 39 | compiler_hash_type : 'hash', 40 | compiler_fail_on_warning : false, 41 | compiler_quiet : false, 42 | compiler_public_path : '/', 43 | compiler_stats : { 44 | chunks : false, 45 | chunkModules : false, 46 | colors : true 47 | }, 48 | compiler_vendors : [ 49 | 'react', 50 | 'react-redux', 51 | 'react-router', 52 | 'redux' 53 | ], 54 | 55 | // ---------------------------------- 56 | // Test Configuration 57 | // ---------------------------------- 58 | coverage_reporters : [ 59 | { type : 'text-summary' }, 60 | { type : 'lcov', dir : 'coverage' } 61 | ] 62 | }; 63 | 64 | /************************************************ 65 | ------------------------------------------------- 66 | 67 | All Internal Configuration Below 68 | Edit at Your Own Risk 69 | 70 | ------------------------------------------------- 71 | ************************************************/ 72 | 73 | // ------------------------------------ 74 | // Environment 75 | // ------------------------------------ 76 | // N.B.: globals added here must _also_ be added to .eslintrc 77 | config.globals = { 78 | 'process.env' : { 79 | 'NODE_ENV' : JSON.stringify(config.env) 80 | }, 81 | 'NODE_ENV' : config.env, 82 | '__DEV__' : config.env === 'development', 83 | '__PROD__' : config.env === 'production', 84 | '__TEST__' : config.env === 'test', 85 | '__COVERAGE__' : !argv.watch && config.env === 'test', 86 | '__BASENAME__' : JSON.stringify(process.env.BASENAME || '') 87 | }; 88 | 89 | // ------------------------------------ 90 | // Validate Vendor Dependencies 91 | // ------------------------------------ 92 | const pkg = require('../package.json'); 93 | 94 | config.compiler_vendors = config.compiler_vendors 95 | .filter((dep) => { 96 | if (pkg.dependencies[dep]) return true; 97 | 98 | debug( 99 | `Package "${dep}" was not found as an npm dependency in package.json; ` + 100 | `it won't be included in the webpack vendor bundle. 101 | Consider removing it from \`compiler_vendors\` in ~/config/index.js` 102 | ); 103 | }); 104 | 105 | // ------------------------------------ 106 | // Utilities 107 | // ------------------------------------ 108 | function base () { 109 | const args = [config.path_base].concat([].slice.call(arguments)); 110 | return path.resolve.apply(path, args); 111 | } 112 | 113 | config.paths = { 114 | base : base, 115 | client : base.bind(null, config.dir_client), 116 | public : base.bind(null, config.dir_public), 117 | dist : base.bind(null, config.dir_dist) 118 | }; 119 | 120 | // ======================================================== 121 | // Environment Configuration 122 | // ======================================================== 123 | debug(`Looking for environment overrides for NODE_ENV "${config.env}".`); 124 | const environments = require('./environments.config'); 125 | const overrides = environments[config.env]; 126 | if (overrides) { 127 | debug('Found overrides, applying to default configuration.'); 128 | Object.assign(config, overrides(config)); 129 | } else { 130 | debug('No environment overrides found, defaults will be used.'); 131 | } 132 | 133 | module.exports = config; 134 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const argv = require('yargs').argv; 2 | const webpack = require('webpack'); 3 | const cssnano = require('cssnano'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const project = require('./project.config'); 7 | const debug = require('debug')('app:config:webpack'); 8 | 9 | const __DEV__ = project.globals.__DEV__; 10 | const __PROD__ = project.globals.__PROD__; 11 | const __TEST__ = project.globals.__TEST__; 12 | 13 | debug('Creating configuration.'); 14 | const webpackConfig = { 15 | name : 'client', 16 | target : 'web', 17 | devtool : project.compiler_devtool, 18 | resolve : { 19 | root : project.paths.client(), 20 | extensions : ['', '.js', '.jsx', '.json'] 21 | }, 22 | module : {} 23 | }; 24 | // ------------------------------------ 25 | // Entry Points 26 | // ------------------------------------ 27 | const APP_ENTRY = project.paths.client('main.js'); 28 | 29 | webpackConfig.entry = { 30 | app : __DEV__ 31 | ? [APP_ENTRY].concat(`webpack-hot-middleware/client?path=${project.compiler_public_path}__webpack_hmr`) 32 | : [APP_ENTRY], 33 | vendor : project.compiler_vendors 34 | }; 35 | 36 | // ------------------------------------ 37 | // Bundle Output 38 | // ------------------------------------ 39 | webpackConfig.output = { 40 | filename : `[name].[${project.compiler_hash_type}].js`, 41 | path : project.paths.dist(), 42 | publicPath : project.compiler_public_path 43 | }; 44 | 45 | // ------------------------------------ 46 | // Externals 47 | // ------------------------------------ 48 | webpackConfig.externals = {}; 49 | webpackConfig.externals['react/lib/ExecutionEnvironment'] = true; 50 | webpackConfig.externals['react/lib/ReactContext'] = true; 51 | webpackConfig.externals['react/addons'] = true; 52 | 53 | // ------------------------------------ 54 | // Plugins 55 | // ------------------------------------ 56 | webpackConfig.plugins = [ 57 | new webpack.DefinePlugin(project.globals), 58 | new HtmlWebpackPlugin({ 59 | template : project.paths.client('index.html'), 60 | hash : false, 61 | filename : 'index.html', 62 | inject : 'body', 63 | minify : { 64 | collapseWhitespace : true 65 | } 66 | }) 67 | ]; 68 | 69 | // Ensure that the compiler exits on errors during testing so that 70 | // they do not get skipped and misreported. 71 | if (__TEST__ && !argv.watch) { 72 | webpackConfig.plugins.push(function () { 73 | this.plugin('done', function (stats) { 74 | if (stats.compilation.errors.length) { 75 | // Pretend no assets were generated. This prevents the tests 76 | // from running making it clear that there were warnings. 77 | throw new Error( 78 | stats.compilation.errors.map(err => err.message || err) 79 | ); 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | if (__DEV__) { 86 | debug('Enabling plugins for live development (HMR, NoErrors).'); 87 | webpackConfig.plugins.push( 88 | new webpack.HotModuleReplacementPlugin(), 89 | new webpack.NoErrorsPlugin() 90 | ); 91 | } else if (__PROD__) { 92 | debug('Enabling plugins for production (OccurenceOrder, Dedupe & UglifyJS).'); 93 | webpackConfig.plugins.push( 94 | new webpack.optimize.OccurrenceOrderPlugin(), 95 | new webpack.optimize.DedupePlugin(), 96 | new webpack.optimize.UglifyJsPlugin({ 97 | compress : { 98 | unused : true, 99 | dead_code : true, 100 | warnings : false 101 | } 102 | }), 103 | new webpack.optimize.AggressiveMergingPlugin() 104 | ); 105 | } 106 | 107 | // Don't split bundles during testing, since we only want import one bundle 108 | if (!__TEST__) { 109 | webpackConfig.plugins.push( 110 | new webpack.optimize.CommonsChunkPlugin({ 111 | names : ['vendor'] 112 | }) 113 | ); 114 | } 115 | 116 | // ------------------------------------ 117 | // Loaders 118 | // ------------------------------------ 119 | // JavaScript / JSON 120 | webpackConfig.module.loaders = [{ 121 | test : /\.(js|jsx)$/, 122 | exclude : /node_modules/, 123 | loader : 'babel', 124 | query : project.compiler_babel 125 | }, { 126 | test : /\.json$/, 127 | loader : 'json' 128 | }]; 129 | 130 | // ------------------------------------ 131 | // Style Loaders 132 | // ------------------------------------ 133 | // We use cssnano with the postcss loader, so we tell 134 | // css-loader not to duplicate minimization. 135 | const BASE_CSS_LOADER = 'css?sourceMap&-minimize'; 136 | 137 | webpackConfig.module.loaders.push({ 138 | test : /\.scss$/, 139 | exclude : null, 140 | loaders : [ 141 | 'style', 142 | BASE_CSS_LOADER, 143 | 'postcss', 144 | 'sass?sourceMap' 145 | ] 146 | }); 147 | webpackConfig.module.loaders.push({ 148 | test : /\.css$/, 149 | exclude : null, 150 | loaders : [ 151 | 'style', 152 | BASE_CSS_LOADER, 153 | 'postcss' 154 | ] 155 | }); 156 | 157 | webpackConfig.sassLoader = { 158 | includePaths : project.paths.client('styles') 159 | }; 160 | 161 | webpackConfig.postcss = [ 162 | cssnano({ 163 | autoprefixer : { 164 | add : true, 165 | remove : true, 166 | browsers : ['last 2 versions'] 167 | }, 168 | discardComments : { 169 | removeAll : true 170 | }, 171 | discardUnused : false, 172 | mergeIdents : false, 173 | reduceIdents : false, 174 | safe : true, 175 | sourcemap : true 176 | }) 177 | ]; 178 | 179 | // File loaders 180 | /* eslint-disable */ 181 | webpackConfig.module.loaders.push( 182 | { test: /\.woff(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff' }, 183 | { test: /\.woff2(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff2' }, 184 | { test: /\.otf(\?.*)?$/, loader: 'file?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=font/opentype' }, 185 | { test: /\.ttf(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/octet-stream' }, 186 | { test: /\.eot(\?.*)?$/, loader: 'file?prefix=fonts/&name=[path][name].[ext]' }, 187 | { test: /\.svg(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=image/svg+xml' }, 188 | { test: /\.(png|jpg)$/, loader: 'url?limit=8192' } 189 | ) 190 | /* eslint-enable */ 191 | 192 | // ------------------------------------ 193 | // Finalize Configuration 194 | // ------------------------------------ 195 | // when we don't know the public path (we know it only when HMR is enabled [in development]) we 196 | // need to use the extractTextPlugin to fix this issue: 197 | // http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts/34133809#34133809 198 | if (!__DEV__) { 199 | debug('Applying ExtractTextPlugin to CSS loaders.'); 200 | webpackConfig.module.loaders.filter((loader) => 201 | loader.loaders && loader.loaders.find((name) => /css/.test(name.split('?')[0])) 202 | ).forEach((loader) => { 203 | const first = loader.loaders[0]; 204 | const rest = loader.loaders.slice(1); 205 | loader.loader = ExtractTextPlugin.extract(first, rest.join('!')); 206 | delete loader.loaders; 207 | }); 208 | 209 | webpackConfig.plugins.push( 210 | new ExtractTextPlugin('[name].[contenthash].css', { 211 | allChunks : true 212 | }) 213 | ); 214 | } 215 | 216 | module.exports = webpackConfig; 217 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-starter-kit", 3 | "version": "3.0.0-alpha.2", 4 | "description": "Get started with React, Redux, and React-Router!", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=4.5.0", 8 | "npm": "^3.0.0" 9 | }, 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "compile": "better-npm-run compile", 13 | "lint": "eslint bin build config server src tests", 14 | "lint:fix": "npm run lint -- --fix; exit 0", 15 | "start": "better-npm-run start", 16 | "dev": "better-npm-run dev", 17 | "test": "better-npm-run test", 18 | "test:dev": "npm run test -- --watch", 19 | "deploy": "better-npm-run deploy", 20 | "deploy:dev": "better-npm-run deploy:dev", 21 | "deploy:prod": "better-npm-run deploy:prod", 22 | "codecov": "cat coverage/*/lcov.info | codecov" 23 | }, 24 | "betterScripts": { 25 | "compile": { 26 | "command": "node bin/compile", 27 | "env": { 28 | "DEBUG": "app:*" 29 | } 30 | }, 31 | "dev": { 32 | "command": "nodemon bin/dev-server --ignore dist --ignore coverage --ignore tests --ignore src", 33 | "env": { 34 | "NODE_ENV": "development", 35 | "DEBUG": "app:*" 36 | } 37 | }, 38 | "deploy": { 39 | "command": "npm run lint && npm run test && npm run clean && npm run compile", 40 | "env": { 41 | "DEBUG": "app:*" 42 | } 43 | }, 44 | "deploy:dev": { 45 | "command": "npm run deploy", 46 | "env": { 47 | "NODE_ENV": "development", 48 | "DEBUG": "app:*" 49 | } 50 | }, 51 | "deploy:prod": { 52 | "command": "npm run deploy", 53 | "env": { 54 | "NODE_ENV": "production", 55 | "DEBUG": "app:*" 56 | } 57 | }, 58 | "start": { 59 | "command": "node bin/dev-server", 60 | "env": { 61 | "DEBUG": "app:*" 62 | } 63 | }, 64 | "test": { 65 | "command": "node ./node_modules/karma/bin/karma start config/karma.config", 66 | "env": { 67 | "NODE_ENV": "test", 68 | "DEBUG": "app:*" 69 | } 70 | } 71 | }, 72 | "repository": { 73 | "type": "git", 74 | "url": "git+https://github.com/davezuko/react-redux-starter-kit.git" 75 | }, 76 | "author": "David Zukowski (http://zuko.me)", 77 | "license": "MIT", 78 | "dependencies": { 79 | "babel-core": "^6.17.0", 80 | "babel-loader": "^6.2.5", 81 | "babel-plugin-transform-runtime": "^6.15.0", 82 | "babel-preset-es2015": "^6.14.0", 83 | "babel-preset-react": "^6.11.1", 84 | "babel-preset-stage-0": "^6.3.13", 85 | "babel-runtime": "^6.11.6", 86 | "better-npm-run": "0.0.13", 87 | "clone": "^2.1.0", 88 | "compression": "^1.6.2", 89 | "css-loader": "^0.26.0", 90 | "cssnano": "^3.7.4", 91 | "debug": "^2.2.0", 92 | "extract-text-webpack-plugin": "^1.0.0", 93 | "file-loader": "^0.9.0", 94 | "formsy-react": "^0.19.2", 95 | "fs-extra": "^1.0.0", 96 | "html-webpack-plugin": "^2.22.0", 97 | "imports-loader": "^0.7.0", 98 | "ip": "^1.1.2", 99 | "json-loader": "^0.5.4", 100 | "moment": "^2.17.1", 101 | "node-sass": "^4.0.0", 102 | "normalize.css": "^5.0.0", 103 | "postcss-loader": "^1.1.0", 104 | "react": "^15.0.0", 105 | "react-datepicker": "^0.41.1", 106 | "react-dom": "^15.0.0", 107 | "react-redux": "^5.0.1", 108 | "react-router": "^3.0.0", 109 | "react-text-mask": "^2.0.0", 110 | "redux": "^3.6.0", 111 | "redux-thunk": "^2.0.0", 112 | "rimraf": "^2.5.4", 113 | "sass-loader": "^4.0.0", 114 | "style-loader": "^0.13.1", 115 | "underscore": "^1.8.3", 116 | "url-loader": "^0.5.6", 117 | "webpack": "^1.12.14", 118 | "yargs": "^6.3.0" 119 | }, 120 | "devDependencies": { 121 | "babel-eslint": "^7.1.0", 122 | "babel-plugin-istanbul": "^3.0.0", 123 | "chai": "^3.4.1", 124 | "chai-as-promised": "^6.0.0", 125 | "chai-enzyme": "^0.6.1", 126 | "cheerio": "^0.22.0", 127 | "codecov": "^1.0.1", 128 | "enzyme": "^2.0.0", 129 | "eslint": "^3.0.1", 130 | "eslint-config-standard": "^6.0.0", 131 | "eslint-config-standard-react": "^4.0.0", 132 | "eslint-plugin-babel": "^4.0.0", 133 | "eslint-plugin-promise": "^3.0.0", 134 | "eslint-plugin-react": "^6.0.0", 135 | "eslint-plugin-standard": "^2.0.0", 136 | "express": "^4.14.0", 137 | "karma": "^1.0.0", 138 | "karma-coverage": "^1.0.0", 139 | "karma-mocha": "^1.0.1", 140 | "karma-mocha-reporter": "^2.0.0", 141 | "karma-phantomjs-launcher": "^1.0.2", 142 | "karma-webpack-with-fast-source-maps": "^1.9.2", 143 | "mocha": "^3.0.1", 144 | "nodemon": "^1.10.2", 145 | "phantomjs-prebuilt": "^2.1.12", 146 | "react-addons-test-utils": "^15.0.0", 147 | "redbox-react": "^1.2.10", 148 | "sinon": "^1.17.5", 149 | "sinon-chai": "^2.8.0", 150 | "webpack-dev-middleware": "^1.6.1", 151 | "webpack-hot-middleware": "^2.12.2" 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/server/main.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const debug = require('debug')('app:server'); 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | const webpackConfig = require('../config/webpack.config'); 6 | const project = require('../config/project.config'); 7 | const compress = require('compression'); 8 | 9 | const app = express(); 10 | 11 | // Apply gzip compression 12 | app.use(compress()); 13 | 14 | // ------------------------------------ 15 | // Apply Webpack HMR Middleware 16 | // ------------------------------------ 17 | if (project.env === 'development') { 18 | const compiler = webpack(webpackConfig); 19 | 20 | debug('Enabling webpack dev and HMR middleware'); 21 | app.use(require('webpack-dev-middleware')(compiler, { 22 | publicPath : webpackConfig.output.publicPath, 23 | contentBase : project.paths.client(), 24 | hot : true, 25 | quiet : project.compiler_quiet, 26 | noInfo : project.compiler_quiet, 27 | lazy : false, 28 | stats : project.compiler_stats 29 | })); 30 | app.use(require('webpack-hot-middleware')(compiler, { 31 | path: '/__webpack_hmr' 32 | })); 33 | 34 | // Serve static assets from ~/public since Webpack is unaware of 35 | // these files. This middleware doesn't need to be enabled outside 36 | // of development since this directory will be copied into ~/dist 37 | // when the application is compiled. 38 | app.use(express.static(project.paths.public())); 39 | 40 | // This rewrites all routes requests to the root /index.html file 41 | // (ignoring file requests). If you want to implement universal 42 | // rendering, you'll want to remove this middleware. 43 | app.use('*', function (req, res, next) { 44 | const filename = path.join(compiler.outputPath, 'index.html'); 45 | compiler.outputFileSystem.readFile(filename, (err, result) => { 46 | if (err) { 47 | return next(err); 48 | } 49 | res.set('content-type', 'text/html'); 50 | res.send(result); 51 | res.end(); 52 | }); 53 | }); 54 | } else { 55 | debug( 56 | 'Server is being run outside of live development mode, meaning it will ' + 57 | 'only serve the compiled application bundle in ~/dist. Generally you ' + 58 | 'do not need an application server for this and can instead use a web ' + 59 | 'server such as nginx to serve your static files. See the "deployment" ' + 60 | 'section in the README for more information on deployment strategies.' 61 | ); 62 | 63 | // Serving ~/dist by default. Ideally these files should be served by 64 | // the web server and not the app server, but this helps to demo the 65 | // server in production. 66 | app.use(express.static(project.paths.dist())); 67 | } 68 | 69 | module.exports = app; 70 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/common/CustomValidators.js: -------------------------------------------------------------------------------- 1 | import Formsy from 'formsy-react' 2 | import moment from 'moment' 3 | 4 | export const isDob = (value) => { 5 | if (value === null) { 6 | return false 7 | } 8 | 9 | return moment(value.format('MM/DD/YYYY')).isValid() 10 | } 11 | 12 | export const isSsn = (value) => { 13 | let cleaned = null 14 | 15 | if (value !== null) { 16 | cleaned = value.toString().replace(/[^0-9.]/g, '') 17 | } 18 | 19 | if (cleaned !== null && cleaned.length === 9) { 20 | return true 21 | } 22 | 23 | return false 24 | } 25 | 26 | export const wireUpCustomFormsyValidators = () => { 27 | Formsy.addValidationRule('isDob', function(values, value) { 28 | return isDob(value) 29 | }) 30 | 31 | Formsy.addValidationRule('isSsn', function(values, value) { 32 | return isSsn(value) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/common/Formatters.js: -------------------------------------------------------------------------------- 1 | export const telephoneFormat = (input) => { 2 | if (!input) { 3 | return 4 | } 5 | 6 | if (input.toString().length === 10) { 7 | let outputTempString = input.toString() 8 | let outputString = outputTempString.substr(0,3) + '-' + outputTempString.substr(3,3) + '-' + 9 | outputTempString.substr(6,4) 10 | return outputString 11 | } else { 12 | return input.toString() 13 | } 14 | } 15 | 16 | export const socialSecurityFormat = (input) => { 17 | if (!input) { 18 | return 19 | } 20 | 21 | var outputString 22 | 23 | if (input.length < 9) { 24 | return input 25 | } else { 26 | let outputTempString = input.toString() 27 | outputString = outputTempString.substr(0,3) + '-' + outputTempString.substr(3,2) + '-' + 28 | outputTempString.substr(5,4) 29 | } 30 | 31 | return outputString; 32 | } 33 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/common/FormsyDatePicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Formsy from 'formsy-react'; 3 | import DatePicker from 'react-datepicker' 4 | 5 | export const FormsyDatePicker = React.createClass({ 6 | mixins: [Formsy.Mixin], 7 | 8 | changeValue(data) { 9 | this.props.onChange(data) 10 | this.setValue(data); 11 | }, 12 | 13 | render() { 14 | const className = this.showRequired() || this.showError() ? 'help-block' : null; 15 | 16 | return ( 17 |
18 | 21 | 22 |
23 | {this.getErrorMessage()} 24 |
25 | ) 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/common/FormsyHiddenInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Formsy from 'formsy-react' 3 | 4 | export const FormsyHiddenInput = React.createClass({ 5 | mixins: [Formsy.Mixin], 6 | 7 | changeValue(event) { 8 | let value = '' 9 | 10 | if (event && event.currentTarget && event.currentTarget.value) { 11 | value = event.currentTarget.value 12 | event.currentTarget.value = value 13 | } 14 | 15 | this.props.onChange(event) 16 | this.setValue(value) 17 | }, 18 | 19 | render() { 20 | return ( 21 |
22 | 26 |
27 | ) 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/common/FormsyInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Formsy from 'formsy-react' 3 | 4 | export const FormsyInput = React.createClass({ 5 | mixins: [Formsy.Mixin], 6 | 7 | sanitize(value) { 8 | if (this.props.sanitizationFunction) { 9 | return this.props.sanitizationFunction(value) 10 | } 11 | 12 | return value 13 | }, 14 | 15 | applyLimitWorkaround(value) { 16 | let limit = this.props.validations.isLength | this.props.validations.maxLength 17 | value = value.slice(0, limit) 18 | return value 19 | }, 20 | 21 | changeValue(event) { 22 | let value = '' 23 | 24 | if (event && event.currentTarget && event.currentTarget.value) { 25 | value = event.currentTarget.value 26 | value = this.sanitize(value) 27 | value = this.applyLimitWorkaround(value) 28 | event.currentTarget.value = value 29 | } 30 | 31 | this.props.onChange(event) 32 | this.setValue(value) 33 | }, 34 | 35 | render() { 36 | const className = this.showRequired() || this.showError() ? 'help-block' : null; 37 | 38 | return ( 39 |
40 | {this.props.label}: 41 | 45 | 46 |
47 | {this.getErrorMessage()} 48 |
49 | ) 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/common/FormsyMaskedInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Formsy from 'formsy-react' 3 | import MaskedInput from 'react-text-mask' 4 | 5 | export const FormsyMaskedInput = React.createClass({ 6 | mixins: [Formsy.Mixin], 7 | 8 | sanitize(value) { 9 | if (this.props.sanitizationFunction) { 10 | return this.props.sanitizationFunction(value) 11 | } 12 | 13 | return value 14 | }, 15 | 16 | applyLimitWorkaround(value) { 17 | let limit = this.props.validations.isLength | this.props.validations.maxLength 18 | value = value.slice(0, limit) 19 | return value 20 | }, 21 | 22 | changeValue(event) { 23 | let value = '' 24 | 25 | if (event && event.currentTarget && event.currentTarget.value) { 26 | value = event.currentTarget.value 27 | value = this.sanitize(value) 28 | value = this.applyLimitWorkaround(value) 29 | event.currentTarget.value = value 30 | } 31 | 32 | this.props.onChange(event) 33 | this.setValue(value) 34 | }, 35 | 36 | render() { 37 | const className = this.showRequired() || this.showError() ? 'help-block' : null 38 | 39 | return ( 40 |
41 | {this.props.label}: 42 | 47 | 48 |
49 | {this.getErrorMessage()} 50 |
51 | ) 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/common/FormsySelect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Formsy from 'formsy-react' 3 | 4 | export const FormsySelect = React.createClass({ 5 | mixins: [Formsy.Mixin], 6 | 7 | changeValue(event) { 8 | let value = '' 9 | 10 | if (event && event.currentTarget && event.currentTarget.value) { 11 | value = event.currentTarget.value 12 | event.currentTarget.value = value 13 | } 14 | 15 | this.props.onChange(event) 16 | this.setValue(value) 17 | }, 18 | 19 | render() { 20 | const options = this.props.options.map((option, i) => ( 21 | 24 | )) 25 | 26 | return ( 27 |
28 | {this.props.label}: 29 | 32 |
33 | ) 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { browserHistory, Router } from 'react-router' 3 | import { Provider } from 'react-redux' 4 | 5 | class AppContainer extends Component { 6 | static propTypes = { 7 | routes : PropTypes.object.isRequired, 8 | store : PropTypes.object.isRequired 9 | } 10 | 11 | shouldComponentUpdate () { 12 | return false 13 | } 14 | 15 | render () { 16 | const { routes, store } = this.props 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | } 25 | 26 | export default AppContainer 27 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Redux Starter Kit 5 | 6 | 7 | 8 | 9 | 10 |
11 |

Patient Overview

12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/layouts/CoreLayout/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './CoreLayout.scss' 3 | import '../../styles/core.scss' 4 | 5 | export const CoreLayout = ({ children }) => (children) 6 | 7 | CoreLayout.propTypes = { 8 | children : React.PropTypes.element.isRequired 9 | } 10 | 11 | export default CoreLayout 12 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/layouts/CoreLayout/CoreLayout.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoTeamEpsilon/angular-to-react-redux/2aed38cb829728585c497bddaf418317c0c296c4/samples/react-redux-patient-demographics-example/src/layouts/CoreLayout/CoreLayout.scss -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/layouts/CoreLayout/index.js: -------------------------------------------------------------------------------- 1 | import CoreLayout from './CoreLayout' 2 | 3 | export default CoreLayout 4 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import createStore from './store/createStore' 4 | import AppContainer from './containers/AppContainer' 5 | 6 | // ======================================================== 7 | // Store Instantiation 8 | // ======================================================== 9 | const initialState = window.___INITIAL_STATE__ 10 | const store = createStore(initialState) 11 | 12 | // ======================================================== 13 | // Render Setup 14 | // ======================================================== 15 | const MOUNT_NODE = document.getElementById('root') 16 | 17 | let render = () => { 18 | const routes = require('./routes/index').default(store) 19 | 20 | ReactDOM.render( 21 | , 22 | MOUNT_NODE 23 | ); 24 | }; 25 | 26 | // This code is excluded from production bundle 27 | if (__DEV__) { 28 | if (module.hot) { 29 | // Development render functions 30 | const renderApp = render 31 | const renderError = (error) => { 32 | const RedBox = require('redbox-react').default 33 | 34 | ReactDOM.render(, MOUNT_NODE) 35 | }; 36 | 37 | // Wrap render in try/catch 38 | render = () => { 39 | try { 40 | renderApp() 41 | } catch (error) { 42 | console.error(error) 43 | renderError(error) 44 | } 45 | }; 46 | 47 | // Setup hot module replacement 48 | module.hot.accept('./routes/index', () => 49 | setImmediate(() => { 50 | ReactDOM.unmountComponentAtNode(MOUNT_NODE) 51 | render() 52 | }) 53 | ); 54 | } 55 | } 56 | 57 | // ======================================================== 58 | // Go! 59 | // ======================================================== 60 | render() 61 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/routes/Patient/Demographics/Basic/BasicComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import moment from 'moment' 3 | import { telephoneFormat, socialSecurityFormat } from '../../../../common/Formatters' 4 | import Formsy from 'formsy-react' 5 | import { wireUpCustomFormsyValidators } from '../../../../common/CustomValidators' 6 | import { FormsyInput } from '../../../../common/FormsyInput' 7 | import { FormsySelect } from '../../../../common/FormsySelect' 8 | import { FormsyDatePicker } from '../../../../common/FormsyDatePicker' 9 | import { FormsyMaskedInput } from '../../../../common/FormsyMaskedInput' 10 | 11 | require('react-datepicker/dist/react-datepicker.css') 12 | 13 | class Basic extends React.Component { 14 | constructor() { 15 | super() 16 | this.state = { 17 | showForm: false, 18 | dob: moment(), 19 | cachedForm: {} 20 | } 21 | 22 | this.handleCancel = this.handleCancel.bind(this) 23 | this.handleInputChange = this.handleInputChange.bind(this) 24 | this.handleEdit = this.handleEdit.bind(this) 25 | wireUpCustomFormsyValidators() 26 | } 27 | 28 | handleEdit() { 29 | console.debug('Basic component in edit mode') 30 | this.setLocalStateToStoreValues() 31 | this.setState({ showForm: true }) 32 | this.setState({ cachedForm: this.props.basic }) 33 | } 34 | 35 | componentDidMount() { 36 | } 37 | 38 | handleSubmit(formValues) { 39 | console.debug('Submitting basic info updates') 40 | // Convert dob back to date string 41 | formValues.dob = formValues.dob.format('YYYY-MM-DD') 42 | this.props.updatePatientData(formValues) 43 | this.setState({ showForm: false }) 44 | } 45 | 46 | handleCancel() { 47 | console.debug('Basic component in read mode') 48 | this.setState({ cachedForm: {} }) 49 | this.setState({ showForm: false }) 50 | } 51 | 52 | sanitizeToJustNumbers(value) { 53 | if (!value) { 54 | return value 55 | } 56 | 57 | return value.replace(/[^0-9.]/g, '') 58 | } 59 | 60 | handleInputChange(event) { 61 | if (event && event.target && event.target.name) { 62 | let value 63 | switch (event.target.name) { 64 | case 'phone': 65 | case 'ssn': 66 | value = this.sanitizeToJustNumbers(event.target.value.toString()) 67 | break 68 | default: 69 | value = event.target.value 70 | } 71 | this.setState({ 72 | [event.target.name]: value 73 | }) 74 | } else { 75 | // Assuming it is the date time picker 76 | this.setState({ 77 | dob: event 78 | }) 79 | } 80 | } 81 | 82 | setLocalStateToStoreValues() { 83 | const keys = ['name', 'dob', 'ssn', 'martialStatus', 'gender', 'address', 'postal', 'city', 'state', 84 | 'country', 'phone', 'email', 'billingNote', 'otherNote'] 85 | 86 | keys.forEach((keyName) => { 87 | let value 88 | 89 | switch (keyName) { 90 | case 'dob': 91 | value = moment(this.props.basic[keyName]) 92 | break 93 | case 'phone': 94 | case 'ssn': 95 | value = this.sanitizeToJustNumbers(this.props.basic[keyName].toString()) 96 | break 97 | default: 98 | value = this.props.basic[keyName] 99 | } 100 | 101 | this.setState({ 102 | [keyName]: value 103 | }) 104 | }) 105 | } 106 | 107 | render() { 108 | if (this.props.basic && this.state.showForm === false) { 109 | return ( 110 |
111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 |
Name: {this.props.basic.name}DOB: {this.props.basic.dob}
SSN: {socialSecurityFormat(this.props.basic.ssn)}Martial Status: {this.props.basic.martialStatus}
Gender: {this.props.basic.gender}Address: {this.props.basic.address}
City: {this.props.basic.address}Postal: {this.props.basic.postal}
State: {this.props.basic.state}Country: {this.props.basic.country}
Phone: {telephoneFormat(this.props.basic.phone)}Email: {this.props.basic.email}
Billing Note: {this.props.basic.billingNote}Other Note: {this.props.basic.otherNote}
143 | 144 | 145 |
146 | ) 147 | } else if (this.props.basic && this.state.showForm === true) { 148 | return ( 149 | 153 | 154 | 155 | 156 | 172 | 187 | 188 | 189 | 205 | 221 | 222 | 223 | 233 | 249 | 250 | 251 | 267 | 283 | 284 | 285 | 301 | 317 | 318 | 319 | 335 | 351 | 352 | 353 | 369 | 385 | 386 | 387 |
157 | 171 | 173 | DOB: 174 | 186 |
190 | 204 | 206 | 220 |
224 | 232 | 234 | 248 |
252 | 266 | 268 | 282 |
286 | 300 | 302 | 316 |
320 | 334 | 336 | 350 |
354 | 368 | 370 | 384 |
388 | 389 | 390 | 393 |
394 | ) 395 | } else { 396 | return null 397 | } 398 | } 399 | } 400 | 401 | export default Basic 402 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/routes/Patient/Demographics/Contact/ContactComponent.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import Formsy from 'formsy-react' 3 | import { FormsyInput } from '../../../../common/FormsyInput' 4 | import { FormsyHiddenInput } from '../../../../common/FormsyHiddenInput' 5 | import { FormsyMaskedInput } from '../../../../common/FormsyMaskedInput' 6 | import { wireUpCustomFormsyValidators } from '../../../../common/CustomValidators' 7 | import {telephoneFormat} from '../../../../common/Formatters' 8 | 9 | class Contact extends Component { 10 | constructor() { 11 | super() 12 | this.state = { 13 | showForm: false, 14 | cachedForm: {} 15 | } 16 | 17 | this.handleCancel = this.handleCancel.bind(this) 18 | this.handleEdit = this.handleEdit.bind(this) 19 | this.handleDelete = this.handleDelete.bind(this) 20 | this.handleInputChange = this.handleInputChange.bind(this) 21 | wireUpCustomFormsyValidators() 22 | } 23 | 24 | handleCancel() { 25 | console.debug(`Contact ${this.props.contact.id} component in cancel mode`) 26 | this.setState({ cachedForm: {} }) 27 | this.setState({ showForm: false }) 28 | } 29 | 30 | handleDelete() { 31 | console.debug(`Contact ${this.props.contact.id} component is being deleted`) 32 | this.props.deleteContact(this.props.contact.id) 33 | } 34 | 35 | handleEdit() { 36 | console.debug(`Contact ${this.props.contact.id} component in edit mode`) 37 | this.setPropsToLocalState() 38 | this.setState({showForm: true}) 39 | this.setState({ cachedForm: this.props.contact }) 40 | } 41 | 42 | componentDidMount() { 43 | if (this.props.contact.isNewContact) { 44 | this.handleEdit() 45 | } 46 | } 47 | 48 | handleInputChange(event) { 49 | let value 50 | if (event.target.name === 'phone') { 51 | value = this.sanitizeToJustNumbers(event.target.value.toString()) 52 | } else { 53 | value = event.target.value 54 | } 55 | this.setState({ 56 | [event.target.name]: value 57 | }) 58 | } 59 | 60 | handleSubmit(formValues) { 61 | console.debug('Submitting contact info updates') 62 | this.props.updateContactData(formValues) 63 | this.setState({ showForm: false }) 64 | } 65 | 66 | sanitizeToJustNumbers(value) { 67 | if (!value) { 68 | return value 69 | } 70 | 71 | return value.replace(/[^0-9.]/g, '') 72 | } 73 | 74 | setPropsToLocalState() { 75 | const keys = ['id', 'name', 'relation', 'address', 'phone', 'city', 'postal', 'state', 'country', 'email'] 76 | 77 | keys.forEach((keyName) => { 78 | let value 79 | // Make switch statement 80 | if (keyName === 'phone') { 81 | value = this.sanitizeToJustNumbers((this.props.contact[keyName] || '').toString()) 82 | } else if (keyName === 'id') { 83 | value = this.props.contact[keyName] 84 | } else { 85 | value = this.props.contact[keyName] 86 | } 87 | 88 | this.setState({ 89 | [keyName]: value 90 | }) 91 | }) 92 | } 93 | 94 | render() { 95 | if (this.props.contact && this.state.showForm === false) { 96 | return ( 97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |
Name: {this.props.contact.name}Relation: {this.props.contact.relation}
Address: {this.props.contact.address}Phone: {telephoneFormat(this.props.contact.phone)}
City: {this.props.contact.city}Postal: {this.props.contact.postal}
State: {this.props.contact.state}Country: {this.props.contact.country}
Email: {this.props.contact.email}
121 | 122 | 123 | 124 | 125 |
126 | 127 |
128 | ) 129 | } else if (this.props.contact && this.state.showForm === true) { 130 | return ( 131 | 132 | 136 | 137 | 138 | 139 | 155 | 171 | 172 | 173 | 188 | 203 | 204 | 205 | 221 | 222 | 223 | 239 | 240 | 257 | 258 | 259 | 275 | 276 | 277 | 293 | 294 | 295 | 301 | 302 | 303 |
140 | 154 | 156 | 170 |
174 | 189 |
206 | 220 |
224 | 238 | 241 | 256 |
260 | 274 |
278 | 292 |
296 | 300 |
304 | 305 | 306 | 309 | 310 |
311 | 312 | ) 313 | } 314 | } 315 | } 316 | 317 | export default Contact 318 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/routes/Patient/Demographics/PatientDemographicsComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { browserHistory } from 'react-router' 3 | import Basic from './Basic/BasicComponent' 4 | import Contact from './Contact/ContactComponent' 5 | 6 | class PatientDemographics extends React.Component { 7 | constructor() { 8 | super() 9 | 10 | this.TABS = { 11 | BASIC: 'basic', 12 | CONTACTS: 'contacts' 13 | } 14 | 15 | this.state = { 16 | tab: this.TABS.BASIC, 17 | isLoading: false 18 | } 19 | } 20 | 21 | setPatientInContext() { 22 | this.setState({ isLoading: true }) 23 | this.props.setPatientInContext(this.props.routeParams.pid) 24 | .then(() => { 25 | this.setState({ isLoading: false }) 26 | }); 27 | } 28 | 29 | addNewContact() { 30 | this.props.startAddingNewContact(this.props.routeParams.pid) 31 | } 32 | 33 | determineIfRouteIsValid() { 34 | return this.props.routeParams.pid 35 | } 36 | 37 | componentDidMount() { 38 | if (!this.determineIfRouteIsValid()) { 39 | browserHistory.push('/patient/1337') 40 | location.reload() 41 | } else { 42 | this.setPatientInContext() 43 | } 44 | } 45 | 46 | mockedTab() { 47 | alert('This tab is just here for completeness. The real tabs are basic and contacts') 48 | } 49 | 50 | changeTab(newTab) { 51 | console.debug(`Setting tab to ${newTab}`) 52 | this.setState({ tab: newTab }) 53 | } 54 | 55 | render() { 56 | let children = null 57 | let addContactVisibility = 'hidden' 58 | 59 | switch (this.state.tab) { 60 | case this.TABS.BASIC: 61 | children = 63 | break; 64 | case this.TABS.CONTACTS: 65 | if (this.props.contacts) { 66 | children = this.props.contacts.map((contact) => { 67 | return 71 | } 72 | ) 73 | } 74 | addContactVisibility = 'visible' 75 | break; 76 | } 77 | 78 | return ( 79 |
80 |

Loading...

81 | 82 |
83 | 97 | 98 | {children} 99 | 100 |
101 | 102 | 105 |
106 |
107 | ) 108 | } 109 | } 110 | 111 | export default PatientDemographics 112 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/routes/Patient/Demographics/PatientDemographicsContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { setPatientInContext, updatePatientData, updateContactData, deleteContact, startAddingNewContact } 3 | from '../PatientModule' 4 | import PatientDemographics from './PatientDemographicsComponent' 5 | 6 | const mapDispatchToProps = { 7 | setPatientInContext, 8 | updatePatientData, 9 | updateContactData, 10 | deleteContact, 11 | startAddingNewContact 12 | } 13 | 14 | const extractBasicInfo = (state) => { 15 | const patient = state.patient[state.patient.patientInContext] 16 | if (patient) { 17 | return patient.basic 18 | } 19 | 20 | return null 21 | } 22 | 23 | const extractContactsInfo = (state) => { 24 | const patient = state.patient[state.patient.patientInContext] 25 | if (patient) { 26 | return patient.contacts 27 | } 28 | 29 | return null 30 | } 31 | 32 | const mapStateToProps = (state) => ({ 33 | patientInContext: state.patient.patientInContext, 34 | basic: extractBasicInfo(state), 35 | contacts: extractContactsInfo(state) 36 | }) 37 | 38 | export default connect(mapStateToProps, mapDispatchToProps)(PatientDemographics) 39 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/routes/Patient/PatientModule.js: -------------------------------------------------------------------------------- 1 | import clone from 'clone' 2 | import _ from 'underscore' 3 | 4 | /** 5 | * Stub data that will be used as the initial state in the store. 6 | */ 7 | const testData = { 8 | // Bug with Formsy: https://github.com/christianalfoni/formsy-react/issues/340 9 | // For some reason, Formsy doesn't perform validations correctly against numbers. 10 | // E.g.: this.state.postal was throwing 'You must not enter more than 50 characters' 11 | // when the value was 54321. When I changed it to '54321', it didn't complain. :(. 12 | 1337: { 13 | basic: { 14 | name: 'John Doe', 15 | dob: '1990-11-04', 16 | ssn: '999999999', 17 | martialStatus: 'Single', 18 | gender: 'Male', 19 | billingNote: 'N/A', 20 | otherNote: 'N/A', 21 | address: '321 Bazbop Ln', 22 | city: 'CoolCity', 23 | postal: '54321', 24 | state: 'Texas', 25 | country: 'US', 26 | phone: '1234567899', 27 | email: 'foo@bar.com' 28 | }, 29 | contacts: [{ 30 | id: 1, 31 | name: 'Jane Doe', 32 | relation: 'Mother', 33 | address: '123 Foobar Ln', 34 | city: 'CoolCity', 35 | postal: '12345', 36 | state: 'Texas', 37 | country: 'US', 38 | phone: '1234567899', 39 | email: 'bar@foo.com' 40 | }, { 41 | id: 2, 42 | name: 'Sam Doe', 43 | relation: 'Father', 44 | address: '123 Foobar Ln', 45 | city: 'CoolCity', 46 | postal: '12345', 47 | state: 'Texas', 48 | country: 'US', 49 | phone: '9876543211', 50 | email: 'baz@bop.com' 51 | }] 52 | } 53 | } 54 | 55 | /** 56 | * Actions 57 | */ 58 | export const setPatientInContext = (patientId) => { 59 | console.info(`attempting to set patient context to patient ${patientId}`) 60 | 61 | return (dispatch, getState) => { 62 | return new Promise((resolve, reject) => { 63 | setTimeout(() => { 64 | const res = testData[patientId] 65 | if (!res) { 66 | var message = `Patient ${patientId} doesn't exist` 67 | console.warn(message) 68 | reject(message); 69 | } else { 70 | console.debug(`Setting patient ${patientId} as patient in context`); 71 | dispatch({ 72 | type : 'UPDATE_PATIENT_IN_CONTEXT', 73 | payload : patientId 74 | }) 75 | 76 | resolve() 77 | } 78 | }, 800) 79 | }) 80 | } 81 | } 82 | 83 | export const updatePatientData = (data) => { 84 | return (dispatch, getState) => { 85 | return new Promise((resolve, reject) => { 86 | console.debug(`updating basic patient data for ${getState().patient.patientInContext}`) 87 | dispatch({ 88 | type : 'UPDATE_PATIENT_DATA', 89 | payload : data 90 | }) 91 | resolve() 92 | }) 93 | } 94 | } 95 | 96 | export const updateContactData = (data) => { 97 | return (dispatch, getState) => { 98 | return new Promise((resolve, reject) => { 99 | console.debug(`updating contact data for ${getState().patient.patientInContext}`) 100 | dispatch({ 101 | type : 'UPDATE_CONTACT_DATA', 102 | payload : data 103 | }) 104 | resolve() 105 | }) 106 | } 107 | } 108 | 109 | export const deleteContact = (data) => { 110 | return (dispatch, getState) => { 111 | return new Promise((resolve, reject) => { 112 | console.debug(`deleting contact data for ${getState().patient.patientInContext}`) 113 | dispatch({ 114 | type : 'DELETE_CONTACT', 115 | payload : data 116 | }) 117 | resolve() 118 | }) 119 | } 120 | } 121 | 122 | export const startAddingNewContact = (data) => { 123 | return (dispatch, getState) => { 124 | return new Promise((resolve, reject) => { 125 | console.debug(`starting to add contact data for ${getState().patient.patientInContext}`) 126 | dispatch({ 127 | type : 'INSERT_CONTACT', 128 | payload : data 129 | }) 130 | resolve() 131 | }) 132 | } 133 | } 134 | 135 | /** 136 | * Reducer 137 | */ 138 | const initialState = testData 139 | export default function patientReducer (state = initialState, action) { 140 | let result 141 | let copy 142 | switch (action.type) { 143 | case 'UPDATE_PATIENT_IN_CONTEXT': 144 | copy = clone(state) 145 | copy.patientInContext = action.payload 146 | result = copy 147 | break 148 | case 'UPDATE_PATIENT_DATA': 149 | copy = clone(state) 150 | copy[copy.patientInContext].basic = action.payload 151 | result = copy 152 | break 153 | case 'UPDATE_CONTACT_DATA': 154 | copy = clone(state) 155 | const contactIndexForUpdation = _.findIndex(copy[copy.patientInContext].contacts, (c) => { 156 | if (c && c.hasOwnProperty('id')) { 157 | return c.id === action.payload.id 158 | } 159 | }) 160 | copy[copy.patientInContext].contacts[contactIndexForUpdation] = action.payload 161 | result = copy 162 | break 163 | case 'INSERT_CONTACT': 164 | copy = clone(state) 165 | const lastContact = _.last(copy[copy.patientInContext].contacts) 166 | let newContactId = 0 167 | if (lastContact != null && lastContact.hasOwnProperty('id')) { 168 | newContactId = lastContact.id + 1 169 | } 170 | copy[copy.patientInContext].contacts.push({ isNewContact: true, id: newContactId }) 171 | result = copy 172 | break 173 | case 'DELETE_CONTACT': 174 | copy = clone(state) 175 | const contactIndexForDeletion = _.findIndex(copy[copy.patientInContext].contacts, (c) => { 176 | if (c && c.hasOwnProperty('id')) { 177 | return c.id === action.payload 178 | } 179 | }) 180 | delete copy[copy.patientInContext].contacts[contactIndexForDeletion] 181 | result = copy 182 | break 183 | default: 184 | result = state 185 | } 186 | 187 | return result 188 | } 189 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/routes/Patient/index.js: -------------------------------------------------------------------------------- 1 | import { injectReducer } from '../../store/reducers'; 2 | 3 | export default (store) => ({ 4 | path : 'patient/:pid', 5 | getComponent (nextState, cb) { 6 | require.ensure([], (require) => { 7 | const patientDemographicsContainer = require( 8 | './Demographics/PatientDemographicsContainer').default 9 | const reducer = require('./PatientModule').default 10 | injectReducer(store, { key: 'patient', reducer }) 11 | cb(null, patientDemographicsContainer) 12 | }, 'patient') 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/routes/index.js: -------------------------------------------------------------------------------- 1 | import CoreLayout from '../layouts/CoreLayout' 2 | import PatientRoute from './Patient' 3 | 4 | export const createRoutes = (store) => ({ 5 | path : '/', 6 | component : CoreLayout, 7 | 8 | // Note: not a good idea to make a route with variables an index, 9 | // just using this for the sample so that /patient/1337 is routed 10 | // to. 11 | indexRoute : PatientRoute(store), 12 | 13 | childRoutes : [ 14 | PatientRoute(store) 15 | ] 16 | }) 17 | 18 | export default createRoutes 19 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { browserHistory } from 'react-router'; 4 | import makeRootReducer from './reducers'; 5 | import { updateLocation } from './location'; 6 | 7 | export default (initialState = {}) => { 8 | // ====================================================== 9 | // Middleware Configuration 10 | // ====================================================== 11 | const middleware = [thunk]; 12 | 13 | // ====================================================== 14 | // Store Enhancers 15 | // ====================================================== 16 | const enhancers = []; 17 | 18 | let composeEnhancers = compose; 19 | 20 | if (__DEV__) { 21 | const composeWithDevToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; 22 | if (typeof composeWithDevToolsExtension === 'function') { 23 | composeEnhancers = composeWithDevToolsExtension; 24 | } 25 | } 26 | 27 | // ====================================================== 28 | // Store Instantiation and HMR Setup 29 | // ====================================================== 30 | const store = createStore( 31 | makeRootReducer(), 32 | initialState, 33 | composeEnhancers( 34 | applyMiddleware(...middleware), 35 | ...enhancers 36 | ) 37 | ); 38 | store.asyncReducers = {}; 39 | 40 | // To unsubscribe, invoke `store.unsubscribeHistory()` anytime 41 | store.unsubscribeHistory = browserHistory.listen(updateLocation(store)); 42 | 43 | if (module.hot) { 44 | module.hot.accept('./reducers', () => { 45 | const reducers = require('./reducers').default; 46 | store.replaceReducer(reducers(store.asyncReducers)); 47 | }); 48 | } 49 | 50 | return store; 51 | }; 52 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/store/location.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Constants 3 | // ------------------------------------ 4 | export const LOCATION_CHANGE = 'LOCATION_CHANGE' 5 | 6 | // ------------------------------------ 7 | // Actions 8 | // ------------------------------------ 9 | export function locationChange (location = '/') { 10 | return { 11 | type : LOCATION_CHANGE, 12 | payload : location 13 | } 14 | } 15 | 16 | // ------------------------------------ 17 | // Specialized Action Creator 18 | // ------------------------------------ 19 | export const updateLocation = ({ dispatch }) => { 20 | return (nextLocation) => dispatch(locationChange(nextLocation)) 21 | } 22 | 23 | // ------------------------------------ 24 | // Reducer 25 | // ------------------------------------ 26 | const initialState = null; 27 | export default function locationReducer (state = initialState, action) { 28 | return action.type === LOCATION_CHANGE 29 | ? action.payload 30 | : state 31 | } 32 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import locationReducer from './location' 3 | 4 | export const makeRootReducer = (asyncReducers) => { 5 | return combineReducers({ 6 | location: locationReducer, 7 | ...asyncReducers 8 | }) 9 | } 10 | 11 | export const injectReducer = (store, { key, reducer }) => { 12 | if (Object.hasOwnProperty.call(store.asyncReducers, key)) return 13 | 14 | store.asyncReducers[key] = reducer 15 | store.replaceReducer(makeRootReducer(store.asyncReducers)) 16 | } 17 | 18 | export default makeRootReducer 19 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Application Settings Go Here 3 | ------------------------------------ 4 | This file acts as a bundler for all variables/mixins/themes, so they 5 | can easily be swapped out without `core.scss` ever having to know. 6 | 7 | For example: 8 | 9 | @import './variables/colors'; 10 | @import './variables/components'; 11 | @import './themes/default'; 12 | */ 13 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/src/styles/core.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | @import '~normalize.css/normalize'; 3 | 4 | // Some best-practice CSS that's useful for most apps 5 | // Just remove them if they're not what you want 6 | html { 7 | box-sizing: border-box; 8 | } 9 | 10 | html, 11 | body { 12 | margin: 0; 13 | padding: 0; 14 | height: 100%; 15 | } 16 | 17 | *, 18 | *:before, 19 | *:after { 20 | box-sizing: inherit; 21 | } 22 | 23 | .help-block { 24 | color: red; 25 | border: red solid 1px; 26 | display: inline-block; 27 | padding: 0 5px; 28 | margin-bottom: 5px; 29 | } 30 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/tests/test-bundler.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Test Environment Setup 3 | // --------------------------------------- 4 | import sinon from 'sinon'; 5 | import chai from 'chai'; 6 | import sinonChai from 'sinon-chai'; 7 | import chaiAsPromised from 'chai-as-promised'; 8 | import chaiEnzyme from 'chai-enzyme'; 9 | 10 | chai.use(sinonChai); 11 | chai.use(chaiAsPromised); 12 | chai.use(chaiEnzyme()); 13 | 14 | global.chai = chai; 15 | global.sinon = sinon; 16 | global.expect = chai.expect; 17 | global.should = chai.should(); 18 | 19 | // --------------------------------------- 20 | // Require Tests 21 | // --------------------------------------- 22 | // for use with karma-webpack-with-fast-source-maps 23 | const __karmaWebpackManifest__ = []; // eslint-disable-line 24 | const inManifest = (path) => ~__karmaWebpackManifest__.indexOf(path); 25 | 26 | // require all `tests/**/*.spec.js` 27 | const testsContext = require.context('./', true, /\.spec\.js$/); 28 | 29 | // only run tests that have changed after the first pass. 30 | const testsToRun = testsContext.keys().filter(inManifest) 31 | ;(testsToRun.length ? testsToRun : testsContext.keys()).forEach(testsContext); 32 | 33 | // require all `src/**/*.js` except for `main.js` (for isparta coverage reporting) 34 | if (__COVERAGE__) { 35 | const componentsContext = require.context('../src/', true, /^((?!main|reducers).)*\.js$/); 36 | componentsContext.keys().forEach(componentsContext); 37 | } 38 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/tests/test.spec.js: -------------------------------------------------------------------------------- 1 | describe('', function() { 2 | it('', function() { 3 | }) 4 | }) 5 | -------------------------------------------------------------------------------- /samples/react-redux-patient-demographics-example/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* This is a sample production webpack config document 2 | // In package.json..."build": "webpack -p --config webpack.config.js" 3 | // This creates an index.html page for you 4 | // It also allows CSS and even SCSS to work 5 | 6 | // You need the follow devdependencies in your package.json (as well as the ones manually included in the file) 7 | "resolve-url-loader" 8 | "sass-loader" 9 | "style-loader" 10 | "url-loader" 11 | 12 | "autoprefixer" 13 | "css-loader" 14 | "file-loader" 15 | "postcss-loader" 16 | */ 17 | 18 | 19 | /* 20 | const path = require('path'); 21 | const webpack = require('webpack'); 22 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 23 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 24 | const autoprefixer = require('autoprefixer'); 25 | 26 | 27 | module.exports = { 28 | 29 | entry: [ 30 | './src/index.js', 31 | ], 32 | 33 | output : { 34 | path : path.join(__dirname, './build'), 35 | filename: 'build.min.js' 36 | }, 37 | 38 | devtool: '#sourcemap', 39 | 40 | module : { 41 | loaders: [{ 42 | exclude: /node_modules/, 43 | loader : 'babel', 44 | query : { 45 | presets: ['react', 'es2015', 'stage-1'] 46 | } 47 | }, 48 | 49 | { 50 | test : /\.css$/, 51 | loader: ExtractTextPlugin.extract('style', 'css') 52 | }, 53 | 54 | { 55 | test: /\.scss$/, 56 | loader: ExtractTextPlugin.extract('style', 'css', 'resolve-url', 'sass') 57 | } 58 | 59 | ] 60 | }, 61 | 62 | postcss: [ 63 | autoprefixer({browsers: ['last 2 versions']}) 64 | ], 65 | 66 | resolve: { 67 | extensions: ['', '.js', '.jsx', '.css'] 68 | }, 69 | 70 | plugins: [ 71 | new HtmlWebpackPlugin({ 72 | title : 'City Info App', 73 | inject: 'body', 74 | }), 75 | new webpack.DefinePlugin({ 76 | 'process.env.VARIABLE': JSON.stringify(process.env.VARIABLE || 'development') 77 | }), 78 | new ExtractTextPlugin("style.css") 79 | ] 80 | }; 81 | */ 82 | --------------------------------------------------------------------------------