├── .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 | [](https://travis-ci.org/GoTeamEpsilon/angular-to-react-redux)
2 |
3 | 
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 | 
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 |