├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ └── update_dependencies.yml ├── .gitignore ├── .mergify.yml ├── .nojekyll ├── .npmignore ├── .npmrc ├── README.md ├── angular.json ├── cypress.config.ts ├── cypress └── e2e │ └── sample_app.cy.js ├── package.json ├── src ├── app │ ├── README.md │ ├── app.angularjs.module.ts │ ├── app.module.ts │ ├── contacts │ │ ├── README.md │ │ ├── contact.component.ts │ │ ├── contactDetail.component.ts │ │ ├── contactList.component.ts │ │ ├── contacts.component.ts │ │ ├── contacts.module.ts │ │ ├── contacts.states.ts │ │ └── editContact.component.ts │ ├── global │ │ ├── README.md │ │ ├── appConfig.service.ts │ │ ├── auth.service.ts │ │ ├── dataSources.service.ts │ │ ├── dialog.directive.ts │ │ ├── dialog.service.ts │ │ ├── global.module.ts │ │ ├── index.ts │ │ └── requiresAuth.hook.ts │ ├── home │ │ ├── README.md │ │ ├── app.component.ts │ │ ├── app.states.ts │ │ ├── home.component.ts │ │ ├── home.module.ts │ │ ├── index.ts │ │ ├── login.component.ts │ │ └── welcome.component.ts │ ├── mymessages │ │ ├── README.md │ │ ├── compose.component.ts │ │ ├── directives │ │ │ ├── index.ts │ │ │ └── sortMessages.directive.ts │ │ ├── filters │ │ │ ├── index.ts │ │ │ └── messageBody.filter.ts │ │ ├── folderList.component.ts │ │ ├── index.ts │ │ ├── message.component.ts │ │ ├── messageList.component.ts │ │ ├── messageTable.component.ts │ │ ├── mymessages.component.ts │ │ ├── mymessages.module.ts │ │ ├── mymessages.states.ts │ │ └── services │ │ │ ├── index.ts │ │ │ └── messagesListUI.service.ts │ ├── prefs │ │ ├── README.md │ │ ├── prefs.component.ts │ │ ├── prefs.module.ts │ │ └── prefs.states.ts │ └── util │ │ ├── README.md │ │ ├── ga.ts │ │ ├── sessionStorage.ts │ │ └── util.ts ├── data │ ├── README.md │ ├── contacts.json │ ├── corpora │ │ ├── 2nd-treatise.txt.gz │ │ ├── beatles.txt.gz │ │ ├── beowulf.txt.gz │ │ ├── bsdfaq.txt.gz │ │ ├── cat-in-the-hat.txt.gz │ │ ├── comm_man.txt.gz │ │ ├── elflore.txt.gz │ │ ├── flatland.txt.gz │ │ ├── green-eggs.txt.gz │ │ ├── macbeth.txt.gz │ │ ├── palin.txt.gz │ │ ├── rfc2549.txt.gz │ │ ├── rfc7230.txt.gz │ │ ├── sneetches.txt.gz │ │ └── two-cities.txt.gz │ ├── fetch.sh │ ├── folders.json │ ├── generate.js │ ├── generate.sh │ └── messages.json ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'CI' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | 11 | ci: 12 | needs: [test] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - run: true 16 | 17 | test: 18 | name: yarn ${{ matrix.yarncmd }} 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | yarncmd: ['test'] 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Configure 26 | run: | 27 | git config --global user.email uirouter@github.actions 28 | git config --global user.name uirouter_github_actions 29 | - name: Install Dependencies 30 | run: yarn install --pure-lockfile 31 | - name: Run Tests 32 | run: yarn ${{ matrix.yarncmd }} 33 | -------------------------------------------------------------------------------- /.github/workflows/update_dependencies.yml: -------------------------------------------------------------------------------- 1 | # This workflow requires a personal access token for uirouterbot 2 | name: Weekly Dependency Bumps 3 | on: 4 | repository_dispatch: 5 | types: [update_dependencies] 6 | schedule: 7 | - cron: '0 19 * * 0' 8 | 9 | jobs: 10 | upgrade: 11 | runs-on: ubuntu-latest 12 | name: 'Update ${{ matrix.deptype }} (latest: ${{ matrix.latest }})' 13 | strategy: 14 | matrix: 15 | excludes: [''] 16 | deptype: ['dependencies', 'devDependencies'] 17 | latest: [false] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - run: | 21 | git config user.name uirouterbot 22 | git config user.password ${{ secrets.UIROUTERBOT_PAT }} 23 | git remote set-url origin $(git remote get-url origin | sed -e 's/ui-router/uirouterbot/') 24 | git fetch --unshallow -p origin 25 | - name: Update dependencies 26 | id: upgrade 27 | uses: ui-router/publish-scripts/actions/upgrade@actions-upgrade-v1.0.3 28 | with: 29 | excludes: ${{ matrix.excludes }} 30 | deptype: ${{ matrix.deptype }} 31 | latest: ${{ matrix.latest }} 32 | - name: Create Pull Request 33 | id: cpr 34 | if: ${{ steps.upgrade.outputs.upgrades != '' }} 35 | # the following hash is from https://github.com/peter-evans/create-pull-request/releases/tag/v2.7.0 36 | uses: peter-evans/create-pull-request@340e629d2f63059fb3e3f15437e92cfbc7acd85b 37 | with: 38 | token: ${{ secrets.UIROUTERBOT_PAT }} 39 | request-to-parent: true 40 | branch-suffix: 'random' 41 | commit-message: 'chore(package): Update ${{ steps.upgrade.outputs.upgradecount }} ${{ matrix.deptype }} to ${{ steps.upgrade.outputs.upgradestrategy }}' 42 | title: 'chore(package): Update ${{ steps.upgrade.outputs.upgradecount }} ${{ matrix.deptype }} to ${{ steps.upgrade.outputs.upgradestrategy }}' 43 | body: | 44 | chore(package): Update ${{ steps.upgrade.outputs.upgradecount }} ${{ matrix.deptype }} to ${{ steps.upgrade.outputs.upgradestrategy }} 45 | 46 | ``` 47 | ${{ steps.upgrade.outputs.upgrades }} 48 | ``` 49 | 50 | Auto-generated by [create-pull-request][1] 51 | 52 | [1]: https://github.com/peter-evans/create-pull-request 53 | - name: Apply Merge Label 54 | if: ${{ steps.cpr.outputs.pr_number != '' }} 55 | uses: actions/github-script@0.9.0 56 | with: 57 | github-token: ${{ secrets.UIROUTERBOT_PAT }} 58 | script: | 59 | await github.issues.addLabels({ 60 | owner: context.repo.owner, 61 | repo: context.repo.repo, 62 | issue_number: ${{ steps.cpr.outputs.pr_number }}, 63 | labels: ['ready to squash and merge'] 64 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.angular/cache 2 | node_modules/ 3 | *.iml 4 | *.ipr 5 | *.iws 6 | .* 7 | transpiled 8 | _bundles 9 | yarn-error.log 10 | 11 | cypress/screenshots 12 | cypress/videos 13 | cypress/fixtures 14 | cypress/plugins 15 | cypress/support 16 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | - check-success=ci 5 | 6 | 7 | pull_request_rules: 8 | - name: Auto Squash and Merge 9 | conditions: 10 | - base=master 11 | - status-success=ci 12 | - 'label=ready to squash and merge' 13 | actions: 14 | delete_head_branch: {} 15 | queue: 16 | method: squash 17 | name: default 18 | - name: Auto Rebase and Merge 19 | conditions: 20 | - base=master 21 | - status-success=ci 22 | - 'label=ready to rebase and merge' 23 | actions: 24 | delete_head_branch: {} 25 | queue: 26 | method: rebase 27 | name: default -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- 1 | nojekyll 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | jspm_packages/ 2 | node_modules/ 3 | *.iml 4 | *.ipr 5 | *.iws 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## UI-Router 1.0 Hybrid Angular-CLI/AngularJS Sample Application 2 | 3 | You can run and edit this live in your browser using https://stackblitz.com/github/ui-router/sample-app-angular-hybrid 4 | 5 | This sample app is intended to demonstrate a non-trivial ui-router application. 6 | 7 | - Multiple sub-modules 8 | - Managed state lifecycle 9 | - Application data lifecycle 10 | - Authentication (simulated) 11 | - Authenticated and unauthenticated states 12 | - REST data retrieval (simulated) 13 | - Lazy loaded sub module (Contacts) 14 | 15 | --- 16 | 17 | ### Running 18 | 19 | `npm install` 20 | `npm start` 21 | 22 | ### Webpack without Angular-CLI 23 | 24 | The sample app uses the Angular CLI. 25 | However, there is also a [branch which demonstrates a custom webpack config](https://github.com/ui-router/sample-app-angular-hybrid/tree/custom-webpack-config)) but no angular-cli. 26 | 27 | ### Visualizer 28 | 29 | We're using the [State and Transition Visualizer](http://github.com/ui-router/visualizer) to visually represent 30 | the current state tree, as well as the transitions between states. Explore how transitions work by hovering 31 | over them, and clicking to expand details (params and resolves). 32 | 33 | Note how states are _entered_ when they were previously not active, _exited_ and re-_entered_ when parameters change, 34 | and how parent states whose parameters did not change are _retained_. Each of these (_exited, entered, retained_) 35 | correspond to a Transition Hook. 36 | 37 | ### Structure 38 | 39 | The application is written in Typescript and utilizes ES6 modules. 40 | We are loading and bundling modules using webpack. 41 | 42 | There are many ways to structure a ui-router app. 43 | We aren't super opinionated on application structure. 44 | Use what works for you. 45 | We organized ours in the following way: 46 | 47 | - Sub-module (feature) organization 48 | - Each feature gets its own directory. 49 | - Features contain states and components 50 | - Specific types of helper code (directives, services, etc) _used only within a feature_ may live in a subdirectory 51 | named after its type (`/mymessages/directives`) 52 | - Leveraging ES6 modules 53 | - Each state is defined in its own file 54 | - Each component (controller + template) is defined in its own file 55 | - Components exported themselves 56 | - Components are then imported into a states where they are composed into the state definition. 57 | - States export themselves 58 | - The `feature.module.js` imports all states for the feature and registers then with the `$stateProvider` 59 | - A single angular module is defined for the entire application 60 | - Created, then exported from `bootstrap/ngmodule.js` 61 | - The ng module is imported into some other module whenever services, config blocks, directives, etc need 62 | to be registered with angular. 63 | 64 | ### UI-Router Patterns 65 | 66 | - Defining custom, app-specific global behaviors 67 | - Add metadata to a state, or state tree 68 | - Check for metadata in transition hooks 69 | - Example: `routerhooks/redirectTo.js` 70 | - If a transition directly to a state with a `redirectTo` property is started, 71 | the transition will be redirected to the state which the property names. 72 | - Example: `routerhooks/authRequired.js` 73 | - If a transition to a state with a truthy `data.authRequired: true` property is started 74 | and the user is not currently authenticated 75 | - Defining a default substate for a top-level state 76 | - Example: declaring `redirectTo: 'mymessages.folder'` in `mymessages/mymessages.states.js` (mymessages state) 77 | - Defining a default parameter for a state 78 | - Example: `folderId` parameter defaults to 'inbox' in `mymessages/mymessages.states.js` (folder state) 79 | - Application data lifecycle 80 | - Data loading is managed by the state declaration, via the `resolve:` block 81 | - Data is fetched before the state is _entered_ 82 | - Data is fetched according to state parameters 83 | - The state is _entered_ when the data is ready 84 | - The resolved data is injected into the components 85 | - The resolve data remains loaded until the state is exited 86 | - Lazy Loading 87 | - The contacts module is an Angular `NgModule`. It is defined as a "future state", and lazy loaded just before any contacts state is activated. 88 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-cli-app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/data", 22 | "src/favicon.ico" 23 | ], 24 | "styles": [ 25 | "src/styles.css" 26 | ], 27 | "scripts": [], 28 | "vendorChunk": true, 29 | "extractLicenses": false, 30 | "buildOptimizer": false, 31 | "sourceMap": true, 32 | "optimization": false, 33 | "namedChunks": true 34 | }, 35 | "configurations": { 36 | "production": { 37 | "budgets": [ 38 | { 39 | "type": "anyComponentStyle", 40 | "maximumWarning": "6kb" 41 | } 42 | ], 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "namedChunks": false, 47 | "extractLicenses": true, 48 | "vendorChunk": false, 49 | "buildOptimizer": true, 50 | "fileReplacements": [ 51 | { 52 | "replace": "src/environments/environment.ts", 53 | "with": "src/environments/environment.prod.ts" 54 | } 55 | ] 56 | } 57 | }, 58 | "defaultConfiguration": "" 59 | }, 60 | "serve": { 61 | "builder": "@angular-devkit/build-angular:dev-server", 62 | "options": { 63 | "buildTarget": "angular-cli-app:build" 64 | }, 65 | "configurations": { 66 | "production": { 67 | "buildTarget": "angular-cli-app:build:production" 68 | } 69 | } 70 | }, 71 | "extract-i18n": { 72 | "builder": "@angular-devkit/build-angular:extract-i18n", 73 | "options": { 74 | "buildTarget": "angular-cli-app:build" 75 | } 76 | }, 77 | "test": { 78 | "builder": "@angular-devkit/build-angular:karma", 79 | "options": { 80 | "main": "src/test.ts", 81 | "karmaConfig": "./karma.conf.js", 82 | "polyfills": "src/polyfills.ts", 83 | "tsConfig": "src/tsconfig.spec.json", 84 | "scripts": [], 85 | "styles": [ 86 | "src/styles.css" 87 | ], 88 | "assets": [ 89 | "src/assets", 90 | "src/data", 91 | "src/favicon.ico" 92 | ] 93 | } 94 | } 95 | } 96 | }, 97 | "angular-cli-app-e2e": { 98 | "root": "e2e", 99 | "sourceRoot": "e2e", 100 | "projectType": "application", 101 | "architect": { 102 | "e2e": { 103 | "builder": "@angular-devkit/build-angular:protractor", 104 | "options": { 105 | "protractorConfig": "./protractor.conf.js", 106 | "devServerTarget": "angular-cli-app:serve" 107 | } 108 | } 109 | } 110 | } 111 | }, 112 | "schematics": { 113 | "@schematics/angular:component": { 114 | "prefix": "app", 115 | "style": "css" 116 | }, 117 | "@schematics/angular:directive": { 118 | "prefix": "app" 119 | } 120 | }, 121 | "cli": { 122 | "analytics": false 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | video: false, 5 | e2e: { 6 | supportFile: false, 7 | setupNodeEvents(on, config) {}, 8 | baseUrl: 'http://localhost:4000', 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /cypress/e2e/sample_app.cy.js: -------------------------------------------------------------------------------- 1 | const EMAIL_ADDRESS = 'myself@angular.dev'; 2 | 3 | describe('unauthenticated sample app', () => { 4 | beforeEach(() => { 5 | window.sessionStorage.clear(); 6 | }); 7 | 8 | it('loads', () => { 9 | cy.visit(''); 10 | }); 11 | 12 | it('renders home', () => { 13 | cy.visit('/#/home'); 14 | cy.get('button.btn').contains('Messages'); 15 | cy.get('button.btn').contains('Contacts'); 16 | cy.get('button.btn').contains('Preferences'); 17 | }); 18 | 19 | it('asks for authentication', () => { 20 | cy.visit('/#/home') 21 | .get('button.btn') 22 | .contains('Preferences') 23 | .click(); 24 | 25 | cy.contains('Log In'); 26 | cy.contains('Username'); 27 | cy.contains('Password'); 28 | 29 | expect(sessionStorage.getItem('appConfig')).to.equal(null); 30 | }); 31 | 32 | it('can authenticate', () => { 33 | expect(sessionStorage.getItem('appConfig')).to.equal(null); 34 | 35 | cy.visit('/#/prefs'); 36 | 37 | cy.contains('Log In'); 38 | cy.contains('Username'); 39 | cy.contains('Password'); 40 | 41 | cy.get('select').contains('myself').parent('select').select(EMAIL_ADDRESS); 42 | cy.get('button').contains('Log in').click(); 43 | 44 | cy.contains('Reset All Data') 45 | .then(() => { 46 | const appConfig = sessionStorage.getItem('appConfig'); 47 | expect(appConfig).not.to.equal(null); 48 | expect(appConfig.emailAddress).not.equal(EMAIL_ADDRESS); 49 | }); 50 | }); 51 | }); 52 | 53 | describe('authenticated sample app', () => { 54 | var _appConfig = null; 55 | beforeEach(() => { 56 | const applyAppConfig = () => { 57 | window.sessionStorage.clear(); 58 | window.sessionStorage.setItem('appConfig', _appConfig); 59 | }; 60 | 61 | if (!_appConfig) { 62 | cy.visit('/#/login'); 63 | cy.get('select').contains('myself').parent('select').select(EMAIL_ADDRESS); 64 | cy.get('button').contains('Log in').click(); 65 | cy.url().should('include', '#/home') 66 | .then(() => { 67 | const appConfig = sessionStorage.getItem('appConfig'); 68 | expect(appConfig).not.to.equal(null); 69 | expect(appConfig.emailAddress).not.equal(EMAIL_ADDRESS); 70 | _appConfig = appConfig; 71 | }) 72 | .then(applyAppConfig); 73 | } else { 74 | applyAppConfig(); 75 | } 76 | }); 77 | 78 | it('navigates to Preferences by url', () => { 79 | cy.visit('/#/prefs'); 80 | cy.contains('Reset All Data'); 81 | }); 82 | 83 | it('navigates to Contacts by url', () => { 84 | cy.visit('/#/contacts'); 85 | cy.contains('Select a contact'); 86 | }); 87 | 88 | it('navigates to Messages by url', () => { 89 | cy.visit('/#/mymessages'); 90 | cy.get('table').contains('Sender'); 91 | cy.get('table').contains('Subject'); 92 | }); 93 | 94 | it('can send a message', () => { 95 | cy.visit('/#/mymessages'); 96 | cy.url().should('include', '#/mymessages/inbox'); 97 | cy.contains('New Message').click(); 98 | cy.url().should('include', '#/mymessages/compose'); 99 | cy.get('input#to').type('somebody@somewhere.com'); 100 | cy.get('input#subject').type('Hello World'); 101 | cy.get('textarea#body').type('The quick brown fox jumps over the lazy dog'); 102 | cy.get('button').contains('Send').click(); 103 | 104 | cy.contains('Sender'); 105 | cy.get('li a').contains('sent').click(); 106 | cy.contains('Hello World'); 107 | cy.get('table').contains('Hello World'); 108 | cy.get('table').contains('somebody@somewhere.com'); 109 | }); 110 | 111 | it('can save a draft', () => { 112 | cy.visit('/#/mymessages'); 113 | cy.url().should('include', '#/mymessages/inbox'); 114 | cy.contains('New Message').click(); 115 | cy.get('input#to').type('somebody@somewhere.com'); 116 | cy.get('input#subject').type('Hello World'); 117 | cy.get('textarea#body').type('The quick brown fox jumps over the lazy dog'); 118 | cy.get('button').contains('Draft').click(); 119 | 120 | cy.contains('Sender'); 121 | cy.get('li a').contains('drafts').click(); 122 | cy.contains('Hello World'); 123 | cy.get('table').contains('Hello World'); 124 | cy.get('table').contains('somebody@somewhere.com'); 125 | }); 126 | 127 | it('prompts to save a message being composed', () => { 128 | cy.visit('/#/mymessages'); 129 | cy.url().should('include', '#/mymessages/inbox'); 130 | cy.contains('New Message').click(); 131 | cy.get('input#to').type('somebody@somewhere.com'); 132 | cy.get('button').contains('Cancel').click(); 133 | 134 | cy.get('.backdrop'); 135 | cy.contains('Navigate away'); 136 | cy.get('button').contains('No').click(); 137 | cy.get('.backdrop').should('not.exist'); 138 | cy.url().should('include', '#/mymessages/compose'); 139 | 140 | 141 | cy.get('button').contains('Cancel').click(); 142 | cy.get('.backdrop'); 143 | cy.contains('Navigate away'); 144 | cy.get('button').contains('Yes').click(); 145 | cy.get('.backdrop').should('not.exist'); 146 | 147 | cy.contains('Sender'); 148 | cy.contains('Subject'); 149 | cy.url().should('include', '#/mymessages/inbox'); 150 | }); 151 | 152 | it('navigates through folders', () => { 153 | cy.visit('/#/mymessages'); 154 | cy.url().should('include', '#/mymessages/inbox'); 155 | 156 | cy.contains('inbox').parent('li').should('have.class', 'selected'); 157 | cy.contains('Longer in style'); 158 | 159 | cy.contains('finance').click().parent('li').should('have.class', 'selected'); 160 | cy.contains('You look angerly'); 161 | cy.url().should('include', '#/mymessages/finance'); 162 | 163 | cy.contains('travel').click().parent('li').should('have.class', 'selected'); 164 | cy.contains('In areas of lush forest'); 165 | cy.url().should('include', '#/mymessages/travel'); 166 | 167 | cy.contains('personal').click().parent('li').should('have.class', 'selected'); 168 | cy.contains('Mother is not all'); 169 | cy.url().should('include', '#/mymessages/personal'); 170 | }); 171 | 172 | it('navigates through messages', () => { 173 | const selectMessage = (subject, guid) => { 174 | cy.contains(subject).click(); 175 | cy.url().should('contain', guid); 176 | cy.get('.message h4').contains(subject); 177 | }; 178 | 179 | cy.visit('/#/mymessages/finance'); 180 | cy.contains('finance').parent('li').should('have.class', 'selected'); 181 | 182 | selectMessage('You look angerly', '5648b50cf8ea6dfc7d1a40a8'); 183 | selectMessage('Historical change consequent', '5648b50c66b80016c9acc467'); 184 | selectMessage('The gracious Duncan', '5648b50d05f033d24fe5a1a2'); 185 | selectMessage('Rings, does not die', '5648b50c8e0e098cef934e04'); 186 | }); 187 | 188 | it('navigates through contacts', () => { 189 | cy.visit('/#/contacts'); 190 | 191 | const selectContact = (name, id) => { 192 | cy.contains(name).click(); 193 | cy.url().should('contain', id); 194 | cy.get('li').contains(name).should('have.class', 'selected'); 195 | cy.get('.contact h3').contains(name); 196 | }; 197 | 198 | selectContact('Rios Sears', 'rsears'); 199 | selectContact('Delia Hunter', 'dhunter'); 200 | selectContact('Underwood Owens', 'uowens'); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-app-angular-hybrid", 3 | "description": "Architecture overview demo for Angular UI-Router", 4 | "version": "1.0.2", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve --port 4000", 8 | "build": "ng build --configuration production", 9 | "test": "run-s e2e", 10 | "lint": "ng lint", 11 | "e2e": "run-p -r start cypress", 12 | "cypress": "wait-on tcp:4000 && cypress run" 13 | }, 14 | "checkPeerDependencies": { 15 | "ignore": [ 16 | "ajv", 17 | "terser" 18 | ] 19 | }, 20 | "license": "MIT", 21 | "dependencies": { 22 | "@angular/animations": "^19.0.5", 23 | "@angular/common": "^19.0.5", 24 | "@angular/compiler": "^19.0.5", 25 | "@angular/core": "^19.0.5", 26 | "@angular/forms": "^19.0.5", 27 | "@angular/platform-browser": "^19.0.5", 28 | "@angular/platform-browser-dynamic": "^19.0.5", 29 | "@angular/router": "^19.0.5", 30 | "@angular/upgrade": "^19.0.5", 31 | "@uirouter/angular": "^16.0.0", 32 | "@uirouter/angular-hybrid": "^18.0.0", 33 | "@uirouter/angularjs": "^1.1.1", 34 | "@uirouter/core": "^6.1.1", 35 | "@uirouter/rx": "1.0.0", 36 | "@uirouter/visualizer": "^7.2.1", 37 | "angular": "1.8.3", 38 | "core-js": "^2.4.1", 39 | "postcss": "8.4.4", 40 | "rxjs": "~7.8.0", 41 | "rxjs-compat": "^6.6.7", 42 | "tslib": "^2.8.1", 43 | "zone.js": "~0.15.0" 44 | }, 45 | "devDependencies": { 46 | "@angular-devkit/build-angular": "^19.0.5", 47 | "@angular/cli": "^19.0.5", 48 | "@angular/compiler-cli": "^19.0.5", 49 | "@angular/language-service": "^19.0.5", 50 | "@types/angular": "^1.8.9", 51 | "@types/jasmine": "~3.10.2", 52 | "@types/jasminewd2": "~2.0.10", 53 | "@types/node": "^16.11.11", 54 | "cypress": "10.3.0", 55 | "jasmine-core": "~3.10.1", 56 | "jasmine-spec-reporter": "~7.0.0", 57 | "karma": "~6.3.9", 58 | "karma-chrome-launcher": "~3.1.0", 59 | "karma-coverage-istanbul-reporter": "~3.0.2", 60 | "karma-jasmine": "~4.0.0", 61 | "karma-jasmine-html-reporter": "^1.7.0", 62 | "npm-run-all": "4.1.5", 63 | "protractor": "~7.0.0", 64 | "shx": "0.3.3", 65 | "ts-node": "~10.9.2", 66 | "tslint": "~6.1.0", 67 | "typescript": "~5.6.3", 68 | "wait-on": "6.0.0" 69 | }, 70 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" 71 | } 72 | -------------------------------------------------------------------------------- /src/app/README.md: -------------------------------------------------------------------------------- 1 | ### Directories 2 | 3 | - *contacts*: The Contacts submodule 4 | - *global*: Global services, router hooks, etc 5 | - *home*: The main submodule, containing the top level states (welcome, login, home) 6 | - *mymessages*: The My Messages submodule 7 | - *prefs*: The Preferences submodule 8 | - *util*: : Utility functions 9 | 10 | ### Files 11 | 12 | - *main*.js: This is where the bundler (webpack) starts 13 | - *angularModule*.ts: The main Angular module that we bootstrap with ngUpgrade 14 | - *angularJSModule*.ts: The main AngularJS module 15 | -------------------------------------------------------------------------------- /src/app/app.angularjs.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file imports the third party library dependencies, then creates the angular module "demo" 3 | * and exports it. 4 | */ 5 | 6 | // External dependencies 7 | import * as angular from "angular"; 8 | import uiRouter from "@uirouter/angularjs"; 9 | import { upgradeModule } from "@uirouter/angular-hybrid"; 10 | 11 | // Feature Modules 12 | import { globalModule } from "./global/index"; 13 | import { homeModule } from "./home/index"; 14 | import { mymessagesModule } from './mymessages/index'; 15 | 16 | // Create the angular 1 module "demo". 17 | // 18 | // Since it is exported, other parts of the application (in other files) can then import it and register things. 19 | // In bootstrap.js, the module is imported, and the components, services, and states are registered. 20 | export const sampleAppModuleAngularJS = angular.module("sampleapp", [ 21 | uiRouter, 22 | upgradeModule.name, 23 | homeModule.name, 24 | globalModule.name, 25 | mymessagesModule.name, 26 | ]); 27 | 28 | // Apply some global configuration... 29 | 30 | // If the user enters a URL that doesn't match any known URL (state), send them to `/welcome` 31 | const otherwiseConfigBlock = ['$urlRouterProvider', '$locationProvider', ($urlRouterProvider, $locationProvider) => { 32 | $locationProvider.hashPrefix(''); 33 | $urlRouterProvider.otherwise("/welcome"); 34 | }]; 35 | sampleAppModuleAngularJS.config(otherwiseConfigBlock); 36 | 37 | // Enable tracing of each TRANSITION... (check the javascript console) 38 | 39 | // This syntax `$trace.enable(1)` is an alternative to `$trace.enable("TRANSITION")`. 40 | // Besides "TRANSITION", you can also enable tracing for : "RESOLVE", "HOOK", "INVOKE", "UIVIEW", "VIEWCONFIG" 41 | const traceRunBlock = ['$trace', $trace => { $trace.enable(1); }]; 42 | sampleAppModuleAngularJS.run(traceRunBlock); 43 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { UpgradeModule } from '@angular/upgrade/static'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | 5 | import { UIRouterUpgradeModule } from '@uirouter/angular-hybrid'; 6 | import { UIRouterModule } from '@uirouter/angular'; 7 | import { sampleAppModuleAngularJS } from './app.angularjs.module'; 8 | 9 | import { PrefsModule } from './prefs/prefs.module'; 10 | 11 | // Create a "future state" (a placeholder) for the Contacts 12 | // Angular module which will be lazy loaded by UI-Router 13 | export const contactsFutureState = { 14 | name: 'contacts.**', 15 | url: '/contacts', 16 | loadChildren: () => import('./contacts/contacts.module').then(m => m.ContactsModule), 17 | }; 18 | 19 | export function getDialogService($injector) { 20 | return $injector.get('DialogService'); 21 | } 22 | 23 | export function getContactsService($injector) { 24 | return $injector.get('Contacts'); 25 | } 26 | 27 | // The main NgModule for the Angular portion of the hybrid app 28 | @NgModule({ 29 | imports: [ 30 | BrowserModule, 31 | // Provide angular upgrade capabilities 32 | UpgradeModule, 33 | UIRouterUpgradeModule, 34 | // Provides the @uirouter/angular directives and registers 35 | // the future state for the lazy loaded contacts module 36 | UIRouterModule.forChild({ states: [contactsFutureState] }), 37 | // The preferences feature module 38 | PrefsModule, 39 | ], 40 | providers: [ 41 | // Register some AngularJS services as Angular providers 42 | { provide: 'DialogService', deps: ['$injector'], useFactory: getDialogService }, 43 | { provide: 'Contacts', deps: ['$injector'], useFactory: getContactsService }, 44 | ] 45 | }) 46 | export class AppModule { 47 | constructor(private upgrade: UpgradeModule) { } 48 | 49 | ngDoBootstrap() { 50 | this.upgrade.bootstrap(document.body, [sampleAppModuleAngularJS.name], { strictDi: true }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/contacts/README.md: -------------------------------------------------------------------------------- 1 | ## Contents 2 | 3 | ### The Contacts submodule states 4 | 5 | - *contacts.states*.js: Defines the Contacts ui-router states 6 | 7 | ### The Contacts submodule components 8 | 9 | - *contactDetail.component*.js: A component which renders a read only view of the details for a single contact. 10 | - *contactList.component*.js: A component which renders a list of contacts 11 | - *contacts.component*.js: A component which renders the contacts submodule. 12 | - *contactView.component*.js: A component which renders details and controls for a single contact 13 | - *editContact.component*.js: A component which edits a single contact. 14 | 15 | ### The index file 16 | 17 | - *index.js*: aggregates and exports the contacts submodule states and components -------------------------------------------------------------------------------- /src/app/contacts/contact.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from "@angular/core"; 2 | 3 | /** 4 | * This component renders details for a single contact 5 | * 6 | * A button messages the contact by linking to `mymessages.compose` state passing the email as a state parameter. 7 | * Another button edits the contact by linking to `contacts.contact.edit` state. 8 | */ 9 | @Component({ 10 | selector: 'contact', 11 | standalone: false, 12 | template: ` 13 |
14 | 15 | 16 | 18 | 21 | 22 | 23 | 26 | 27 |
28 | `}) 29 | export class Contact { 30 | @Input() contact; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/contacts/contactDetail.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from "@angular/core"; 2 | 3 | /** 4 | * This component renders a read only view of the details for a single contact. 5 | */ 6 | @Component({ 7 | selector: 'contact-detail', 8 | standalone: false, 9 | template: ` 10 |
11 |
12 |

{{contact.name.first}} {{contact.name.last}}

13 |
{{contact.company}}
14 |
{{contact.age}}
15 |
{{contact.phone}}
16 |
{{contact.email}}
17 |
18 | 19 |
{{contact.address.street}}
20 | {{contact.address.city}}, {{contact.address.state}} {{contact.address.zip}} 21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 | ` 30 | }) 31 | export class ContactDetail { 32 | @Input() contact; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/contacts/contactList.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from "@angular/core"; 2 | 3 | /** 4 | * This component renders a list of contacts. 5 | * 6 | * At the top is a "new contact" button. 7 | * Each list item is a clickable link to the `contacts.contact` details substate 8 | */ 9 | @Component({ 10 | selector: 'contact-list', 11 | standalone: false, 12 | template: ` 13 | 34 | `}) 35 | export class ContactList { 36 | @Input() contacts; 37 | } 38 | -------------------------------------------------------------------------------- /src/app/contacts/contacts.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from "@angular/core"; 2 | 3 | /** 4 | * This component renders the contacts submodule. 5 | * 6 | * On the left is the list of contacts. 7 | * On the right is the ui-view viewport where contact details appear. 8 | */ 9 | @Component({ 10 | selector: 'contacts', 11 | standalone: false, 12 | template: ` 13 |
14 | 15 | 16 | 17 |
18 | 19 |

Select a contact

20 |
21 |
22 | `}) 23 | export class Contacts { 24 | @Input() contacts; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/contacts/contacts.module.ts: -------------------------------------------------------------------------------- 1 | import {FormsModule} from '@angular/forms'; 2 | import {CommonModule} from "@angular/common"; 3 | import {NgModule} from "@angular/core"; 4 | import {UIRouterModule} from "@uirouter/angular"; 5 | 6 | import {contactsState, editContactState, newContactState, viewContactState} from "./contacts.states"; 7 | import {ContactDetail} from "./contactDetail.component"; 8 | import {ContactList} from "./contactList.component"; 9 | import {Contact} from "./contact.component"; 10 | import {Contacts} from "./contacts.component"; 11 | import {EditContact} from "./editContact.component"; 12 | 13 | export let CONTACTS_STATES = [contactsState, newContactState, viewContactState, editContactState]; 14 | 15 | /** The NgModule for Contacts feature */ 16 | @NgModule({ 17 | imports: [ 18 | CommonModule, 19 | FormsModule, 20 | UIRouterModule.forChild({ states: CONTACTS_STATES }) 21 | ], 22 | declarations: [Contact, ContactDetail, ContactList, Contacts, EditContact], 23 | }) 24 | class ContactsModule {} 25 | 26 | export {ContactsModule}; 27 | -------------------------------------------------------------------------------- /src/app/contacts/contacts.states.ts: -------------------------------------------------------------------------------- 1 | import {Ng2StateDeclaration} from "@uirouter/angular"; 2 | 3 | import {Contact} from "./contact.component"; 4 | import {Contacts} from "./contacts.component"; 5 | import {EditContact} from "./editContact.component"; 6 | 7 | 8 | // Resolve all the contacts. The resolved contacts are injected into the controller. 9 | resolveContacts.$inject = ['Contacts']; 10 | export function resolveContacts(Contacts) { 11 | return Contacts.all(); 12 | } 13 | 14 | /** 15 | * This state displays the contact list. 16 | * It also provides a nested ui-view (viewport) for child states to fill in. 17 | * 18 | * The contacts are fetched using a resolve. 19 | */ 20 | export const contactsState: Ng2StateDeclaration = { 21 | parent: 'app', // declares that 'contacts' is a child of 'app' 22 | name: "contacts", 23 | url: "/contacts", 24 | resolve: { 25 | contacts: resolveContacts 26 | }, 27 | data: { requiresAuth: true }, 28 | component: Contacts 29 | }; 30 | 31 | resolveContact.$inject = ['Contacts', '$transition$']; 32 | export function resolveContact(Contacts, $transition$) { 33 | return Contacts.get($transition$.params().contactId); 34 | } 35 | 36 | /** 37 | * This state displays a single contact. 38 | * The contact to display is fetched using a resolve, based on the `contactId` parameter. 39 | */ 40 | export const viewContactState: Ng2StateDeclaration = { 41 | name: 'contacts.contact', 42 | url: '/:contactId', 43 | resolve: { 44 | // Resolve the contact, based on the contactId parameter value. 45 | // The resolved contact is provided to the contactComponent's contact binding 46 | contact: resolveContact 47 | }, 48 | component: Contact 49 | }; 50 | 51 | 52 | /** 53 | * This state allows a user to edit a contact 54 | * 55 | * The contact data to edit is injected from the parent state's resolve. 56 | * 57 | * This state uses view targeting to replace the parent ui-view (which would normally be filled 58 | * by 'contacts.contact') with the edit contact template/controller 59 | */ 60 | export const editContactState: Ng2StateDeclaration = { 61 | name: 'contacts.contact.edit', 62 | url: '/edit', 63 | views: { 64 | // Relatively target the grand-parent-state's $default (unnamed) ui-view 65 | // This could also have been written using ui-view@state addressing: $default@contacts 66 | // Or, this could also have been written using absolute ui-view addressing: !$default.$default.$default 67 | '^.^.$default': { 68 | bindings: { pristineContact: "contact" }, 69 | component: EditContact 70 | } 71 | } 72 | }; 73 | 74 | export function resolvePristineContact() { 75 | return { name: {}, address: {} }; 76 | } 77 | /** 78 | * This state allows a user to create a new contact 79 | * 80 | * The contact data to edit is injected into the component from the parent state's resolve. 81 | */ 82 | export const newContactState: Ng2StateDeclaration = { 83 | name: 'contacts.new', 84 | url: '/new', 85 | resolve: { 86 | pristineContact: resolvePristineContact 87 | }, 88 | component: EditContact 89 | }; 90 | -------------------------------------------------------------------------------- /src/app/contacts/editContact.component.ts: -------------------------------------------------------------------------------- 1 | import * as angular from "angular"; 2 | import {UIView} from "@uirouter/angular"; 3 | import {StateService, TransitionService} from "@uirouter/core"; 4 | import {Component, Input, Inject, Optional} from "@angular/core"; 5 | 6 | /** 7 | * The EditContact component 8 | * 9 | * This component is used by both `contacts.contact.edit` and `contacts.new` states. 10 | * 11 | * The component makes a copy of the contqct data for editing. 12 | * The new copy and original (pristine) copy are used to determine if the contact is "dirty" or not. 13 | * If the user navigates to some other state while the contact is "dirty", the `uiCanExit` component 14 | * hook asks the user to confirm navigation away, losing any edits. 15 | * 16 | * The Delete Contact button is wired to the `remove` method, which: 17 | * - asks for confirmation from the user 18 | * - deletes the resource from REST API 19 | * - navigates back to the contacts grandparent state using relative addressing `^.^` 20 | * the `reload: true` option re-fetches the contacts list from the server 21 | * 22 | * The Save Contact button is wired to the `save` method which: 23 | * - saves the REST resource (PUT or POST, depending) 24 | * - navigates back to the parent state using relative addressing `^`. 25 | * when editing an existing contact, this returns to the `contacts.contact` "view contact" state 26 | * when creating a new contact, this returns to the `contacts` list. 27 | * the `reload: true` option re-fetches the contacts resolve data from the server 28 | */ 29 | @Component({ 30 | selector: 'edit-contact', 31 | standalone: false, 32 | template: ` 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 | export class EditContact { 59 | @Input() pristineContact; 60 | contact; 61 | state; 62 | deregister; 63 | canExit: boolean; 64 | 65 | // Note: you can inject StateService and TransitionService from @uirouter/core 66 | constructor(public $state: StateService, 67 | @Inject('DialogService') public DialogService, 68 | @Inject('Contacts') public Contacts, 69 | @Optional() @Inject(UIView.PARENT_INJECT) view, 70 | public $trans: TransitionService) { 71 | this.state = view && view.context && view.context.name; 72 | } 73 | 74 | ngOnInit() { 75 | // Make an editable copy of the pristineContact 76 | this.contact = angular.copy(this.pristineContact); 77 | this.deregister = this.$trans.onBefore({ exiting: this.state }, () => this.uiCanExit()); 78 | } 79 | 80 | ngOnDestroy() { 81 | if (this.deregister) this.deregister(); 82 | } 83 | 84 | uiCanExit() { 85 | if (this.canExit || angular.equals(this.contact, this.pristineContact)) { 86 | return true; 87 | } 88 | 89 | let message = 'You have unsaved changes to this contact.'; 90 | let question = 'Navigate away and lose changes?'; 91 | return this.DialogService.confirm(message, question); 92 | } 93 | 94 | /** Ask for confirmation, then delete the contact, then go to the grandparent state ('contacts') */ 95 | remove(contact) { 96 | this.DialogService.confirm(`Delete contact: ${contact.name.first} ${contact.name.last}`) 97 | .then(() => this.Contacts.remove(contact)) 98 | .then(() => this.canExit = true) 99 | .then(() => this.$state.go("^.^", null, { reload: true })); 100 | } 101 | 102 | /** Save the contact, then go to the parent state (either 'contacts' or 'contacts.contact') */ 103 | save(contact) { 104 | this.Contacts.save(contact) 105 | .then(() => this.canExit = true) 106 | .then(() => this.$state.go("^", null, { reload: true })); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/app/global/README.md: -------------------------------------------------------------------------------- 1 | ## Contents 2 | 3 | ### Global services 4 | - *appConfig*.service.js: Stores and retrieves the user's application preferences 5 | - *auth*.service.js: Simulates an authentication service 6 | - *dataSources*.service.js: Provides REST-like client API for Folders, Messages, and Contacts 7 | - *dialog*.service.js: Provides a dialog confirmation service 8 | 9 | ### Directives 10 | - *dialog*.directive.js: Provides a dialog directive used by the dialog service 11 | 12 | ### Router Hooks 13 | 14 | - *requiresAuth*.hook.js: A transition hook which allows a state to declare that it requires an authenticated user -------------------------------------------------------------------------------- /src/app/global/appConfig.service.ts: -------------------------------------------------------------------------------- 1 | import * as angular from "angular"; 2 | import { globalModule } from './global.module'; 3 | 4 | /** 5 | * This service stores and retrieves user preferences in session storage 6 | */ 7 | export class AppConfig { 8 | sort: string = '+date'; 9 | emailAddress: string = undefined; 10 | restDelay: number = 100; 11 | 12 | constructor() { 13 | this.load(); 14 | } 15 | 16 | load() { 17 | try { 18 | return angular.extend(this, angular.fromJson(sessionStorage.getItem("appConfig"))) 19 | } catch (Error) { } 20 | 21 | return this; 22 | } 23 | 24 | save() { 25 | sessionStorage.setItem("appConfig", angular.toJson(angular.extend({}, this))); 26 | } 27 | } 28 | 29 | globalModule.service('AppConfig', AppConfig); 30 | -------------------------------------------------------------------------------- /src/app/global/auth.service.ts: -------------------------------------------------------------------------------- 1 | import {AppConfig} from "./appConfig.service"; 2 | import { globalModule } from './global.module'; 3 | 4 | /** 5 | * This service emulates an Authentication Service. 6 | */ 7 | export class AuthService { 8 | // data 9 | usernames: string[] = ['myself@angular.dev', 'devgal@angular.dev', 'devguy@angular.dev']; 10 | 11 | static $inject = ['AppConfig', '$q', '$timeout']; 12 | constructor(public AppConfig: AppConfig, public $q, public $timeout) { } 13 | 14 | /** 15 | * Returns true if the user is currently authenticated, else false 16 | */ 17 | isAuthenticated() { 18 | return !!this.AppConfig.emailAddress; 19 | } 20 | 21 | /** 22 | * Fake authentication function that returns a promise that is either resolved or rejected. 23 | * 24 | * Given a username and password, checks that the username matches one of the known 25 | * usernames (this.usernames), and that the password matches 'password'. 26 | * 27 | * Delays 800ms to simulate an async REST API delay. 28 | */ 29 | authenticate(username, password) { 30 | let { $timeout, $q, AppConfig } = this; 31 | 32 | // checks if the username is one of the known usernames, and the password is 'password' 33 | const checkCredentials = () => $q((resolve, reject) => { 34 | var validUsername = this.usernames.indexOf(username) !== -1; 35 | var validPassword = password === 'password'; 36 | 37 | return (validUsername && validPassword) ? resolve(username) : reject("Invalid username or password"); 38 | }); 39 | 40 | return $timeout(checkCredentials, 800) 41 | .then((authenticatedUser) => { 42 | AppConfig.emailAddress = authenticatedUser; 43 | AppConfig.save() 44 | }); 45 | } 46 | 47 | /** Logs the current user out */ 48 | logout() { 49 | this.AppConfig.emailAddress = undefined; 50 | this.AppConfig.save(); 51 | } 52 | } 53 | 54 | globalModule.service('AuthService', AuthService); 55 | -------------------------------------------------------------------------------- /src/app/global/dataSources.service.ts: -------------------------------------------------------------------------------- 1 | import {SessionStorage} from "../util/sessionStorage" 2 | import {AppConfig} from "./appConfig.service"; 3 | import { globalModule } from './global.module'; 4 | 5 | /** 6 | * Fake REST Services (Contacts, Folders, Messages) used in the mymessages submodule. 7 | * 8 | * Each of these APIs have: 9 | * 10 | * .all() 11 | * .search(exampleItem) 12 | * .get(id) 13 | * .save(item) 14 | * .post(item) 15 | * .put(item) 16 | * .remove(item) 17 | * 18 | * See ../util/sessionStorage.js for more details, if curious 19 | */ 20 | 21 | /** A fake Contacts REST client API */ 22 | export class Contacts extends SessionStorage { 23 | static $inject = ['$http', '$timeout', '$q', 'AppConfig']; 24 | constructor($http, $timeout, $q, AppConfig: AppConfig) { 25 | // http://beta.json-generator.com/api/json/get/V1g6UwwGx 26 | super($http, $timeout, $q, "contacts", "data/contacts.json", AppConfig); 27 | } 28 | } 29 | 30 | /** A fake Folders REST client API */ 31 | export class Folders extends SessionStorage { 32 | static $inject = ['$http', '$timeout', '$q', 'AppConfig']; 33 | constructor($http, $timeout, $q, AppConfig) { 34 | super($http, $timeout, $q, 'folders', 'data/folders.json', AppConfig); 35 | } 36 | } 37 | 38 | export class Messages extends SessionStorage { 39 | static $inject = ['$http', '$timeout', '$q', 'AppConfig']; 40 | constructor($http, $timeout, $q, public AppConfig: AppConfig) { 41 | // http://beta.json-generator.com/api/json/get/VJl5GbIze 42 | super($http, $timeout, $q, 'messages', 'data/messages.json', AppConfig); 43 | } 44 | 45 | byFolder(folder) { 46 | let searchObject = { folder: folder._id }; 47 | let toFromAttr = ["drafts", "sent"].indexOf(folder._id) !== -1 ? "from" : "to"; 48 | searchObject[toFromAttr] = this.AppConfig.emailAddress; 49 | return this.search(searchObject); 50 | } 51 | } 52 | 53 | globalModule.service('Contacts', Contacts); 54 | globalModule.service('Folders', Folders); 55 | globalModule.service('Messages', Messages); 56 | -------------------------------------------------------------------------------- /src/app/global/dialog.directive.ts: -------------------------------------------------------------------------------- 1 | import { globalModule } from './global.module'; 2 | 3 | dialogDirective.$inject = ['$timeout', '$q']; 4 | function dialogDirective($timeout, $q) { 5 | return { 6 | link: (scope, elem) => { 7 | $timeout(() => elem.addClass('active')); 8 | elem.data('promise', $q((resolve, reject) => { 9 | scope.yes = () => resolve(true); 10 | scope.no = () => reject(false); 11 | })); 12 | }, 13 | template: ` 14 |
15 |
16 |
17 |

{{message}}

18 |
{{details}}
19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 | ` 27 | } 28 | } 29 | 30 | globalModule.directive('dialog', dialogDirective); 31 | -------------------------------------------------------------------------------- /src/app/global/dialog.service.ts: -------------------------------------------------------------------------------- 1 | import * as angular from "angular"; 2 | import { globalModule } from './global.module'; 3 | 4 | export class DialogService { 5 | confirm; 6 | 7 | static $inject = ['$document', '$compile', '$rootScope']; 8 | constructor($document, $compile, $rootScope) { 9 | let body = $document.find("body"); 10 | let elem = angular.element("
"); 11 | 12 | this.confirm = (message, details = "Are you sure?", yesMsg = "Yes", noMsg = "No") => { 13 | $compile(elem)(angular.extend($rootScope.$new(), {message, details, yesMsg, noMsg})); 14 | body.append(elem); 15 | return elem.data("promise").finally(() => elem.removeClass('active').remove()); 16 | } 17 | } 18 | } 19 | 20 | globalModule.service('DialogService', DialogService); 21 | -------------------------------------------------------------------------------- /src/app/global/global.module.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | export const globalModule = angular.module('global', ['ui.router']); 3 | -------------------------------------------------------------------------------- /src/app/global/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./global.module"; 2 | 3 | import "./appConfig.service"; 4 | import "./auth.service"; 5 | import "./dataSources.service"; 6 | import "./dialog.directive"; 7 | import "./dialog.service"; 8 | import "./requiresAuth.hook"; 9 | -------------------------------------------------------------------------------- /src/app/global/requiresAuth.hook.ts: -------------------------------------------------------------------------------- 1 | import { globalModule } from './global.module'; 2 | 3 | /** 4 | * This file contains a Transition Hook which protects a 5 | * route that requires authentication. 6 | * 7 | * This hook redirects to /login when both: 8 | * - The user is not authenticated 9 | * - The user is navigating to a state that requires authentication 10 | */ 11 | authHookRunBlock.$inject = ['$transitions', 'AuthService']; 12 | function authHookRunBlock($transitions, AuthService) { 13 | // Matches if the destination state's data property has a truthy 'requiresAuth' property 14 | let requiresAuthCriteria = { 15 | to: (state) => state.data && state.data.requiresAuth 16 | }; 17 | 18 | // Function that returns a redirect for the current transition to the login state 19 | // if the user is not currently authenticated (according to the AuthService) 20 | 21 | let redirectToLogin = (transition) => { 22 | let AuthService = transition.injector().get('AuthService'); 23 | let $state = transition.router.stateService; 24 | if (!AuthService.isAuthenticated()) { 25 | return $state.target('login', undefined, { location: false }); 26 | } 27 | }; 28 | 29 | // Register the "requires auth" hook with the TransitionsService 30 | $transitions.onBefore(requiresAuthCriteria, redirectToLogin, {priority: 10}); 31 | } 32 | 33 | globalModule.run(authHookRunBlock); 34 | -------------------------------------------------------------------------------- /src/app/home/README.md: -------------------------------------------------------------------------------- 1 | ## Contents 2 | 3 | ### The main app module bootstrap 4 | 5 | - *app.states*.js: Defines the top-level states such as home, welcome, and login 6 | 7 | ### Components for the Top-level states 8 | 9 | - *app.component*.js: A component which displays the header nav-bar for authenticated in users 10 | - *home.component*.js: A component that has links to the main submodules 11 | - *login.component*.js: A component for authenticating a guest user 12 | - *welcome.component*.js: A component which displays a welcome screen for guest users 13 | -------------------------------------------------------------------------------- /src/app/home/app.component.ts: -------------------------------------------------------------------------------- 1 | import { homeModule } from './home.module'; 2 | 3 | /** 4 | * The controller for the `app` component. 5 | */ 6 | class AuthedController { 7 | //data 8 | emailAddress; 9 | isAuthenticated; 10 | 11 | static $inject = ['AppConfig', 'AuthService', '$state']; 12 | constructor(AppConfig, public AuthService, public $state) { 13 | this.emailAddress = AppConfig.emailAddress; 14 | this.isAuthenticated = AuthService.isAuthenticated(); 15 | } 16 | 17 | logout() { 18 | let {AuthService, $state} = this; 19 | AuthService.logout(); 20 | // Reload states after authentication change 21 | return $state.go('welcome', {}, { reload: true }); 22 | } 23 | } 24 | 25 | /** 26 | * This is the main app component for an authenticated user. 27 | * 28 | * This component renders the outermost chrome (application header and tabs, the compose and logout button) 29 | * It has a `ui-view` viewport for nested states to fill in. 30 | */ 31 | const appComponent = { 32 | controller: AuthedController, 33 | template: ` 34 | 57 | 58 |
59 | ` 60 | }; 61 | 62 | homeModule.component('app', appComponent); 63 | -------------------------------------------------------------------------------- /src/app/home/app.states.ts: -------------------------------------------------------------------------------- 1 | import { homeModule } from './home.module'; 2 | 3 | /** 4 | * This is the parent state for the entire application. 5 | * 6 | * This state's primary purposes are: 7 | * 1) Shows the outermost chrome (including the navigation and logout for authenticated users) 8 | * 2) Provide a viewport (ui-view) for a substate to plug into 9 | */ 10 | const appState = { 11 | name: 'app', 12 | redirectTo: 'welcome', 13 | component: 'app' 14 | }; 15 | 16 | /** 17 | * This is the 'welcome' state. It is the default state (as defined by app.js) if no other state 18 | * can be matched to the URL. 19 | */ 20 | const welcomeState = { 21 | parent: 'app', 22 | name: 'welcome', 23 | url: '/welcome', 24 | component: 'welcome' 25 | }; 26 | 27 | 28 | /** 29 | * This is a home screen for authenticated users. 30 | * 31 | * It shows giant buttons which activate their respective submodules: Messages, Contacts, Preferences 32 | */ 33 | const homeState = { 34 | parent: 'app', 35 | name: 'home', 36 | url: '/home', 37 | component: 'home' 38 | }; 39 | 40 | 41 | /** 42 | * This is the login state. It is activated when the user navigates to /login, or if a unauthenticated 43 | * user attempts to access a protected state (or substate) which requires authentication. (see routerhooks/requiresAuth.js) 44 | * 45 | * It shows a fake login dialog and prompts the user to authenticate. Once the user authenticates, it then 46 | * reactivates the state that the user originally came from. 47 | */ 48 | const loginState = { 49 | parent: 'app', 50 | name: 'login', 51 | url: '/login', 52 | component: 'login', 53 | resolve: { returnTo: returnTo } 54 | }; 55 | 56 | /** 57 | * A resolve function for 'login' state which figures out what state to return to, after a successful login. 58 | * 59 | * If the user was initially redirected to login state (due to the requiresAuth redirect), then return the toState/params 60 | * they were redirected from. Otherwise, if they transitioned directly, return the fromState/params. Otherwise 61 | * return the main "home" state. 62 | */ 63 | returnTo.$inject = ['$transition$']; 64 | function returnTo ($transition$): any { 65 | if ($transition$.redirectedFrom() != null) { 66 | // The user was redirected to the login state (e.g., via the requiresAuth hook when trying to activate contacts) 67 | // Return to the original attempted target state (e.g., contacts) 68 | return $transition$.redirectedFrom().targetState(); 69 | } 70 | 71 | let $state = $transition$.router.stateService; 72 | 73 | // The user was not redirected to the login state; they directly activated the login state somehow. 74 | // Return them to the state they came from. 75 | if ($transition$.from().name !== '') { 76 | return $state.target($transition$.from(), $transition$.params("from")); 77 | } 78 | 79 | // If the fromState's name is empty, then this was the initial transition. Just return them to the home state 80 | return $state.target('home'); 81 | } 82 | 83 | homeModule.config(['$stateProvider', ($stateProvider) => { 84 | $stateProvider.state(appState); 85 | $stateProvider.state(welcomeState); 86 | $stateProvider.state(homeState); 87 | $stateProvider.state(loginState); 88 | }]); 89 | -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { homeModule } from './home.module'; 2 | 3 | // This is a home component for authenticated users. 4 | // It shows giant buttons which activate their respective submodules: Messages, Contacts, Preferences 5 | const homeComponent = { 6 | template: ` 7 |
8 | 12 | 13 | 17 | 18 | 22 |
23 | `}; 24 | 25 | homeModule.component('home', homeComponent); 26 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | export const homeModule = angular.module('main', ['ui.router']); 3 | -------------------------------------------------------------------------------- /src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./home.module"; 2 | 3 | import "./app.component"; 4 | import "./app.states"; 5 | import "./home.component"; 6 | import "./login.component"; 7 | import "./welcome.component"; 8 | -------------------------------------------------------------------------------- /src/app/home/login.component.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | 3 | import { homeModule } from './home.module'; 4 | import { TargetState } from "@uirouter/core"; 5 | 6 | /** 7 | * The controller for the `login` component 8 | * 9 | * The `login` method validates the credentials. 10 | * Then it sends the user back to the `returnTo` state, which is provided as a resolve data. 11 | */ 12 | class LoginController { 13 | returnTo: TargetState; 14 | 15 | usernames; 16 | credentials; 17 | authenticating; 18 | errorMessage; 19 | 20 | login; 21 | 22 | static $inject = ['AppConfig', 'AuthService', '$state']; 23 | constructor(AppConfig, AuthService, $state) { 24 | this.usernames = AuthService.usernames; 25 | 26 | this.credentials = { 27 | username: AppConfig.emailAddress, 28 | password: 'password' 29 | }; 30 | 31 | this.login = (credentials) => { 32 | this.authenticating = true; 33 | 34 | const returnToOriginalState = () => { 35 | let state = this.returnTo.state(); 36 | let params = this.returnTo.params(); 37 | let options = angular.extend({}, this.returnTo.options(), { reload: true }); 38 | $state.go(state, params, options); 39 | }; 40 | 41 | const showError = (errorMessage) => 42 | this.errorMessage = errorMessage; 43 | 44 | AuthService.authenticate(credentials.username, credentials.password) 45 | .then(returnToOriginalState) 46 | .catch(showError) 47 | .finally(() => this.authenticating = false); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * This component renders a faux authentication UI 54 | * 55 | * It prompts for the username/password (and gives hints with bouncy arrows) 56 | * It shows errors if the authentication failed for any reason. 57 | */ 58 | const loginComponent = { 59 | bindings: { returnTo: '<' }, 60 | 61 | controller: LoginController, 62 | 63 | template: ` 64 |
65 |
66 |

Log In

67 |

(This login screen is for demonstration only... just pick a username, enter 'password' and click "Log in")

68 |
69 | 70 |
71 | 72 | 74 | Choose 76 |
77 |
78 | 79 |
80 | 81 | 82 | 84 | Enter 'password' here 85 | 86 |
87 | 88 |
{{ $ctrl.errorMessage }}
89 | 90 |
91 |
92 | 96 | Click Me! 97 |
98 |
99 | ` 100 | }; 101 | 102 | homeModule.component('login', loginComponent); 103 | -------------------------------------------------------------------------------- /src/app/home/welcome.component.ts: -------------------------------------------------------------------------------- 1 | import { homeModule } from './home.module'; 2 | 3 | const welcomeComponent = { 4 | template: ` 5 |
6 | 7 |

UI-Router Sample App

8 | 9 |

Welcome to the sample app!

10 |

This is a demonstration app intended to highlight some patterns that can be used within UI-Router. 11 | These patterns should help you to to build cohesive, robust apps. Additionally, this app uses state-vis 12 | to show the tree of states, and a transition log visualizer.

13 | 14 |

App Overview

15 |

16 | First, start exploring the application's functionality at a high level by activating 17 | one of the three submodules: Messages, Contacts, or Preferences. If you are not already logged in, 18 | you will be taken to an authentication screen (the authentication is fake; the password is "password") 19 |

20 | 21 | 22 | 23 |
24 |

25 | 26 |

Patterns and Recipes

27 |
    28 |
  • Require Authentication
  • 29 |
  • Previous State
  • 30 |
  • Redirect Hook
  • 31 |
  • Default Param Values
  • 32 |
33 |
` 34 | }; 35 | 36 | homeModule.component('welcome', welcomeComponent); 37 | -------------------------------------------------------------------------------- /src/app/mymessages/README.md: -------------------------------------------------------------------------------- 1 | ## Contents 2 | 3 | ### States 4 | 5 | - *mymessages.states.ts*: The MyMessages states 6 | 7 | ### Components 8 | 9 | - *compose.component*.ts: Edits a new message 10 | - *folderList.component*.ts: Displays a list of folders. 11 | - *message.component*.ts: Displays the contents of a message. 12 | - *messageList.component*.ts: Displays a list of messages. 13 | - *messageTable.component*.ts: Displays a folder of messages as a table. 14 | - *mymessages.component*.ts: Displays a list of folders. 15 | 16 | ### Filters 17 | 18 | - *filters/messageBody.filter*.ts: Converts plain text formatting to something that html can display nicer. 19 | 20 | ### Directives 21 | 22 | - *directives/sortMessages*.js: A directive used in messageTable to toggle the currently sorted column 23 | 24 | ### Services 25 | 26 | - *services/messageListUI.service*.ts: A service used to find the closest (next/prev) message to the current message 27 | -------------------------------------------------------------------------------- /src/app/mymessages/compose.component.ts: -------------------------------------------------------------------------------- 1 | import * as angular from "angular"; 2 | import { mymessagesModule } from './mymessages.module'; 3 | 4 | /** 5 | * The controller for the Compose component 6 | */ 7 | class ComposeController { 8 | // bound 9 | $stateParams; 10 | $transition$; 11 | 12 | // data 13 | pristineMessage; 14 | message; 15 | canExit: boolean; 16 | 17 | static $inject = ['$state', 'DialogService', 'AppConfig', 'Messages']; 18 | constructor(public $state, public DialogService, public AppConfig, public Messages) { } 19 | 20 | /** 21 | * Create our message's model using the current user's email address as 'message.from' 22 | * Then extend it with all the properties from (non-url) state parameter 'message'. 23 | * Keep two copies: the editable one and the original one. 24 | * These copies are used to check if the message is dirty. 25 | */ 26 | $onInit() { 27 | this.pristineMessage = angular.extend({from: this.AppConfig.emailAddress}, this.$stateParams.message); 28 | this.message = angular.copy(this.pristineMessage); 29 | } 30 | 31 | /** 32 | * Checks if the edited copy and the pristine copy are identical when the state is changing. 33 | * If they are not identical, the allows the user to confirm navigating away without saving. 34 | */ 35 | uiCanExit() { 36 | if (this.canExit || angular.equals(this.pristineMessage, this.message)) { 37 | return true; 38 | } 39 | 40 | var message = 'You have not saved this message.'; 41 | var question = 'Navigate away and lose changes?'; 42 | return this.DialogService.confirm(message, question, "Yes", "No"); 43 | } 44 | 45 | /** 46 | * Navigates back to the previous state. 47 | * 48 | * - Checks the $transition$ which activated this controller for a 'from state' that isn't the implicit root state. 49 | * - If there is no previous state (because the user deep-linked in, etc), then go to 'mymessages.messagelist' 50 | */ 51 | gotoPreviousState() { 52 | let $transition$ = this.$transition$; 53 | let hasPrevious = !!$transition$.from().name; 54 | let state = hasPrevious ? $transition$.from() : "mymessages.messagelist"; 55 | let params = hasPrevious ? $transition$.params("from") : {}; 56 | this.$state.go(state, params); 57 | }; 58 | 59 | /** "Send" the message (save to the 'sent' folder), and then go to the previous state */ 60 | send(message) { 61 | this.Messages.save(angular.extend(message, {date: new Date(), read: true, folder: 'sent'})) 62 | .then(() => this.canExit = true) 63 | .then(() => this.gotoPreviousState()); 64 | }; 65 | 66 | /** Save the message to the 'drafts' folder, and then go to the previous state */ 67 | save(message) { 68 | this.Messages.save(angular.extend(message, {date: new Date(), read: true, folder: 'drafts'})) 69 | .then(() => this.canExit = true) 70 | .then(() => this.gotoPreviousState()); 71 | } 72 | } 73 | 74 | /** 75 | * This component composes a message 76 | * 77 | * The message might be new, a saved draft, or a reply/forward. 78 | * A Cancel button discards the new message and returns to the previous state. 79 | * A Save As Draft button saves the message to the "drafts" folder. 80 | * A Send button sends the message 81 | */ 82 | const composeComponent = { 83 | bindings: { $stateParams: '<', $transition$: '<' }, 84 | 85 | controller: ComposeController, 86 | 87 | template: ` 88 |
89 |
90 |
91 |
92 |
93 | 94 |
95 | 96 | 97 |
98 | 99 | 100 | 101 | 102 |
103 |
104 |
105 | ` 106 | }; 107 | 108 | mymessagesModule.component('compose', composeComponent); 109 | -------------------------------------------------------------------------------- /src/app/mymessages/directives/index.ts: -------------------------------------------------------------------------------- 1 | import "./sortMessages.directive"; 2 | -------------------------------------------------------------------------------- /src/app/mymessages/directives/sortMessages.directive.ts: -------------------------------------------------------------------------------- 1 | import * as angular from "angular"; 2 | import { mymessagesModule } from '../mymessages.module'; 3 | 4 | /** 5 | * A directive (for a table header) which changes the app's sort order 6 | */ 7 | sortMessagesDirective.$inject = ['AppConfig']; 8 | function sortMessagesDirective(AppConfig) { 9 | return { 10 | restrict: 'A', 11 | link: function(scope, elem, attrs) { 12 | let col = attrs['sortMessages']; 13 | if (!col) return; 14 | let chevron = angular.element(""); 15 | elem.append(chevron); 16 | 17 | elem.on("click", (evt) => AppConfig.sort = (AppConfig.sort === `+${col}`) ? `-${col}` : `+${col}`); 18 | scope.$watch(() => AppConfig.sort, (newVal) => { 19 | chevron.toggleClass("fa-sort-asc", newVal == `+${col}`); 20 | chevron.toggleClass("fa-sort-desc", newVal == `-${col}`); 21 | }); 22 | } 23 | } 24 | }; 25 | 26 | mymessagesModule.directive('sortMessages', sortMessagesDirective); 27 | -------------------------------------------------------------------------------- /src/app/mymessages/filters/index.ts: -------------------------------------------------------------------------------- 1 | import "./messageBody.filter"; -------------------------------------------------------------------------------- /src/app/mymessages/filters/messageBody.filter.ts: -------------------------------------------------------------------------------- 1 | import { mymessagesModule } from '../mymessages.module'; 2 | 3 | messageBody.$inject = ['$sce']; 4 | function messageBody($sce) { 5 | return (msgText = '') => $sce.trustAsHtml(msgText.split(/\n/).map(p => `

${p}

`).join('\n')); 6 | } 7 | 8 | mymessagesModule.filter('messageBody', messageBody); 9 | -------------------------------------------------------------------------------- /src/app/mymessages/folderList.component.ts: -------------------------------------------------------------------------------- 1 | import { mymessagesModule } from './mymessages.module'; 2 | 3 | /** 4 | * Renders a list of folders 5 | */ 6 | const folderListComponent = { 7 | bindings: {folders: '<'}, 8 | 9 | template: ` 10 | 11 |
12 | 23 |
24 | `}; 25 | 26 | mymessagesModule.component('folderList', folderListComponent); 27 | -------------------------------------------------------------------------------- /src/app/mymessages/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mymessages.module"; 2 | 3 | import "./compose.component"; 4 | import "./folderList.component"; 5 | import "./message.component"; 6 | import "./messageList.component"; 7 | import "./messageTable.component"; 8 | import "./mymessages.component"; 9 | import "./mymessages.states"; 10 | 11 | import "./directives/index"; 12 | import "./filters/index"; 13 | import "./services/index"; 14 | -------------------------------------------------------------------------------- /src/app/mymessages/message.component.ts: -------------------------------------------------------------------------------- 1 | import {setProp} from "../util/util"; 2 | import { mymessagesModule } from './mymessages.module'; 3 | 4 | 5 | /** Helper function to prefix a message with "fwd: " or "re: " */ 6 | const prefixSubject = (prefix, message) => prefix + message.subject; 7 | /** Helper function which quotes an email message */ 8 | const quoteMessage = (message) => ` 9 | 10 | 11 | 12 | --------------------------------------- 13 | Original message: 14 | From: ${message.from} 15 | Date: ${message.date} 16 | Subject: ${message.subject} 17 | 18 | ${message.body}`; 19 | 20 | /** Helper function to make a response message object */ 21 | const makeResponseMsg = (subjectPrefix, origMsg) => ({ 22 | from: origMsg.to, 23 | to: origMsg.from, 24 | subject: prefixSubject(subjectPrefix, origMsg), 25 | body: quoteMessage(origMsg) 26 | }); 27 | 28 | 29 | /** 30 | * The controller for the Message component 31 | */ 32 | class MessageController { 33 | // bound 34 | folder; 35 | message; 36 | nextMessageGetter; 37 | 38 | // data 39 | actions; 40 | 41 | static $inject = ['$state', 'DialogService', 'Messages']; 42 | constructor(public $state, public DialogService, public Messages) { } 43 | 44 | /** 45 | * When the user views a message, mark it as read and save (PUT) the resource. 46 | * 47 | * Apply the available actions for the message, depending on the folder the message belongs to. 48 | */ 49 | $onInit() { 50 | this.message.read = true; 51 | this.Messages.put(this.message); 52 | 53 | this.actions = this.folder.actions.reduce((obj, action) => setProp(obj, action, true), {}); 54 | } 55 | 56 | /** 57 | * Compose a new message as a reply to this one 58 | */ 59 | reply(message) { 60 | let replyMsg = makeResponseMsg("Re: ", message); 61 | this.$state.go('mymessages.compose', { message: replyMsg }); 62 | }; 63 | 64 | /** 65 | * Compose a new message as a forward of this one. 66 | */ 67 | forward(message) { 68 | let fwdMsg = makeResponseMsg("Fwd: ", message); 69 | delete fwdMsg.to; 70 | this.$state.go('mymessages.compose', { message: fwdMsg }); 71 | }; 72 | 73 | /** 74 | * Continue composing this (draft) message 75 | */ 76 | editDraft(message) { 77 | this.$state.go('mymessages.compose', { message: message }); 78 | }; 79 | 80 | /** 81 | * Delete this message. 82 | * 83 | * - confirm deletion 84 | * - delete the message 85 | * - determine which message should be active 86 | * - show that message 87 | */ 88 | remove(message) { 89 | let nextMessageId = this.nextMessageGetter(message._id); 90 | let nextState = nextMessageId ? 'mymessages.messagelist.message' : 'mymessages.messagelist'; 91 | let params = { messageId: nextMessageId }; 92 | 93 | this.DialogService.confirm("Delete?", undefined) 94 | .then(() => this.Messages.remove(message)) 95 | .then(() => this.$state.go(nextState, params, { reload: 'mymessages.messagelist' })); 96 | }; 97 | } 98 | 99 | /** 100 | * This component renders a single message 101 | * 102 | * Buttons perform actions related to the message. 103 | * Buttons are shown/hidden based on the folder's context. 104 | * For instance, a "draft" message can be edited, but can't be replied to. 105 | */ 106 | export const messageComponent = { 107 | bindings: { folder: '<', message: '<', nextMessageGetter: '<' }, 108 | 109 | controller: MessageController, 110 | 111 | template: ` 112 |
113 | 114 |
115 |
116 |

{{$ctrl.message.subject}}

117 |
{{$ctrl.message.from}} {{$ctrl.message.to}}
118 |
119 | 120 |
121 |
{{$ctrl.message.date | date: 'longDate'}} {{$ctrl.message.date | date: 'mediumTime'}}
122 |
123 | 124 | 125 | 126 | 127 |
128 |
129 |
130 | 131 | 132 |
133 |
134 | `}; 135 | 136 | mymessagesModule.component('message', messageComponent); 137 | -------------------------------------------------------------------------------- /src/app/mymessages/messageList.component.ts: -------------------------------------------------------------------------------- 1 | import { mymessagesModule } from './mymessages.module'; 2 | 3 | /** 4 | * This component renders a list of messages using the `messageTable` component 5 | */ 6 | const messageListComponent = { 7 | bindings: { folder: '<', messages: '<' }, 8 | template: ` 9 |
10 | 11 |
12 | `}; 13 | 14 | mymessagesModule.component('messageList', messageListComponent); -------------------------------------------------------------------------------- /src/app/mymessages/messageTable.component.ts: -------------------------------------------------------------------------------- 1 | import { mymessagesModule } from './mymessages.module'; 2 | 3 | messageTableController.$inject = ['AppConfig']; 4 | function messageTableController(AppConfig) { 5 | this.AppConfig = AppConfig; 6 | this.colVisible = (name) => this.columns.indexOf(name) !== -1; 7 | } 8 | 9 | /** 10 | * A component that displays a folder of messages as a table 11 | * 12 | * If a row is clicked, the details of the message is shown using a relative ui-sref to `.message`. 13 | * 14 | * ui-sref-active is used to highlight the selected row. 15 | * 16 | * Shows/hides specific columns based on the `columns` input binding. 17 | */ 18 | const messageTableComponent = { 19 | bindings: { columns: '<', messages: '<' }, 20 | 21 | controller: messageTableController, 22 | 23 | template: ` 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
SenderRecipientSubjectDate
{{ message.from }}{{ message.to }}{{ message.subject }}{{ message.date | date: "yyyy-MM-dd" }}
47 | `}; 48 | 49 | mymessagesModule.component('messageTable', messageTableComponent); 50 | -------------------------------------------------------------------------------- /src/app/mymessages/mymessages.component.ts: -------------------------------------------------------------------------------- 1 | import { mymessagesModule } from './mymessages.module'; 2 | 3 | /** 4 | * The main mymessages component. 5 | * 6 | * Renders a list of folders, and has two viewports: 7 | * - messageList: filled with the list of messages for a folder 8 | * - messagecontent: filled with the contents of a single message. 9 | */ 10 | const mymessagesComponent = { 11 | bindings: {folders: '<'}, 12 | 13 | template: ` 14 |
15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 | `}; 27 | 28 | mymessagesModule.component('mymessages', mymessagesComponent); 29 | -------------------------------------------------------------------------------- /src/app/mymessages/mymessages.module.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | 3 | // The angularjs module for 'mymessages' 4 | // This module is imported in each of the 5 | export const mymessagesModule = angular.module('mymessages', ['ui.router']); 6 | -------------------------------------------------------------------------------- /src/app/mymessages/mymessages.states.ts: -------------------------------------------------------------------------------- 1 | import { mymessagesModule } from './mymessages.module'; 2 | 3 | /** 4 | * This state allows the user to compose a new message, edit a drafted message, send a message, 5 | * or save an unsent message as a draft. 6 | * 7 | * This state uses view-targeting to take over the ui-view that would normally be filled by the 'mymessages' state. 8 | */ 9 | const composeState = { 10 | name: 'mymessages.compose', 11 | url: '/compose', 12 | // Declares that this state has a 'message' parameter, that defaults to an empty object. 13 | // Note the parameter does not appear in the URL. 14 | params: { 15 | message: {} 16 | }, 17 | views: { 18 | // Absolutely targets the $default (unnamed) ui-view, two nesting levels down with the composeComponent. 19 | "!$default.$default": 'compose' 20 | } 21 | }; 22 | 23 | /** 24 | * The mymessages state. This is the main state for the mymessages submodule. 25 | * 26 | * This state shows the list of folders for the current user. It retrieves the folders from the 27 | * Folders service. If a user navigates directly to this state, the state redirects to the 'mymessages.messagelist'. 28 | */ 29 | const mymessagesState = { 30 | parent: 'app', 31 | name: "mymessages", 32 | url: "/mymessages", 33 | resolve: { 34 | // All the folders are fetched from the Folders service 35 | folders: ['Folders', (Folders) => { 36 | return Folders.all(); 37 | }], 38 | }, 39 | // If mymessages state is directly activated, redirect the transition to the child state 'mymessages.messagelist' 40 | redirectTo: 'mymessages.messagelist', 41 | component: 'mymessages', 42 | // Mark this state as requiring authentication. See ../routerhooks/requiresAuth.js. 43 | data: { requiresAuth: true } 44 | }; 45 | 46 | 47 | /** 48 | * This state shows the contents of a single message. 49 | * It also has UI to reply, forward, delete, or edit an existing draft. 50 | */ 51 | const messageState = { 52 | name: 'mymessages.messagelist.message', 53 | url: '/:messageId', 54 | resolve: { 55 | // Fetch the message from the Messages service using the messageId parameter 56 | message: ['Messages', '$stateParams', (Messages, $stateParams) => { 57 | return Messages.get($stateParams.messageId); 58 | }], 59 | // Provide the component with a function it can query that returns the closest message id 60 | nextMessageGetter: ['MessageListUI', 'messages', (MessageListUI, messages) => { 61 | return MessageListUI.proximalMessageId.bind(MessageListUI, messages); 62 | }], 63 | }, 64 | views: { 65 | // Relatively target the parent-state's parent-state's 'messagecontent' ui-view 66 | // This could also have been written using ui-view@state addressing: 'messagecontent@mymessages' 67 | // Or, this could also have been written using absolute ui-view addressing: '!$default.$default.messagecontent' 68 | "^.^.messagecontent": 'message' 69 | } 70 | }; 71 | 72 | 73 | /** 74 | * This state shows the contents (a message list) of a single folder 75 | */ 76 | const messageListState = { 77 | name: 'mymessages.messagelist', 78 | url: '/:folderId', 79 | // The folderId parameter is part of the URL. This params block sets 'inbox' as the default value. 80 | // If no parameter value for folderId is provided on the transition, then it will be defaulted to 'inbox' 81 | params: {folderId: "inbox"}, 82 | resolve: { 83 | // Fetch the current folder from the Folders service, using the folderId parameter 84 | folder: ['Folders', '$stateParams', (Folders, $stateParams) => { 85 | return Folders.get($stateParams.folderId); 86 | }], 87 | 88 | // The resolved folder object (from the resolve above) is injected into this resolve 89 | // The list of message for the folder are fetched from the Messages service 90 | messages: ['Messages', 'folder', (Messages, folder) => { 91 | return Messages.byFolder(folder); 92 | }], 93 | }, 94 | views: { 95 | // This targets the "messagelist" named ui-view added to the DOM in the parent state 'mymessages' 96 | "messagelist": 'messageList' 97 | } 98 | }; 99 | 100 | mymessagesModule.config(['$stateProvider', ($stateProvider) => { 101 | $stateProvider.state(composeState); 102 | $stateProvider.state(mymessagesState); 103 | $stateProvider.state(messageState); 104 | $stateProvider.state(messageListState); 105 | }]); 106 | -------------------------------------------------------------------------------- /src/app/mymessages/services/index.ts: -------------------------------------------------------------------------------- 1 | import "./messagesListUI.service"; -------------------------------------------------------------------------------- /src/app/mymessages/services/messagesListUI.service.ts: -------------------------------------------------------------------------------- 1 | import { mymessagesModule } from '../mymessages.module'; 2 | 3 | /** Provides services related to a message list */ 4 | class MessageListUI { 5 | static $inject = ['$filter', 'AppConfig']; 6 | constructor(public $filter, public AppConfig) { } 7 | 8 | /** This is a UI helper which finds the nearest messageId in the messages list to the messageId parameter */ 9 | proximalMessageId(messages, messageId) { 10 | let sorted = this.$filter("orderBy")(messages, this.AppConfig.sort); 11 | let idx = sorted.findIndex(msg => msg._id === messageId); 12 | var proximalIdx = sorted.length > idx + 1 ? idx + 1 : idx - 1; 13 | return proximalIdx >= 0 ? sorted[proximalIdx]._id : undefined; 14 | } 15 | } 16 | 17 | mymessagesModule.service('MessageListUI', MessageListUI); 18 | -------------------------------------------------------------------------------- /src/app/prefs/README.md: -------------------------------------------------------------------------------- 1 | This is an Angular module which is imported into the bootstrapped Angular Module 2 | 3 | ## Contents 4 | 5 | ### Module 6 | 7 | - *prefs.module*.js: Defines the Angular Module for the Prefs feature module 8 | 9 | ### States 10 | 11 | - *prefs.state*.ts: A template/controller for showing and/or updating user preferences. 12 | 13 | ### Components 14 | 15 | - *prefs.component*.ts: Displays and updates user preferences. 16 | -------------------------------------------------------------------------------- /src/app/prefs/prefs.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'prefs-component', 5 | template: ` 6 |
7 | 8 |
9 | 10 |
11 | 12 | 13 | 14 |
15 | `, 16 | standalone: false, 17 | }) 18 | export class PrefsComponent { 19 | prefs; 20 | 21 | constructor(@Inject('AppConfig') public AppConfig) { } 22 | 23 | ngOnInit() { 24 | this.prefs = { 25 | restDelay: this.AppConfig.restDelay 26 | } 27 | } 28 | 29 | /** Clear out the session storage */ 30 | reset() { 31 | sessionStorage.clear(); 32 | document.location.reload(); 33 | } 34 | 35 | /** After saving preferences to session storage, reload the entire application */ 36 | savePrefs() { 37 | Object.assign(this.AppConfig, { restDelay: this.prefs.restDelay }).save(); 38 | document.location.reload(); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/app/prefs/prefs.module.ts: -------------------------------------------------------------------------------- 1 | import { PrefsComponent } from './prefs.component'; 2 | import { prefsState } from './prefs.states'; 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { UIRouterModule } from '@uirouter/angular'; 7 | 8 | let PREFS_STATES = [ prefsState ]; 9 | 10 | /** The NgModule for the Preferences feature */ 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | FormsModule, 15 | UIRouterModule.forChild({ states: PREFS_STATES }) 16 | ], 17 | declarations: [ PrefsComponent ], 18 | }) 19 | class PrefsModule {} 20 | 21 | export {PrefsModule}; -------------------------------------------------------------------------------- /src/app/prefs/prefs.states.ts: -------------------------------------------------------------------------------- 1 | import { PrefsComponent } from './prefs.component'; 2 | 3 | /** 4 | * This state allows the user to set their application preferences 5 | */ 6 | export const prefsState = { 7 | parent: 'app', 8 | name: 'prefs', 9 | url: '/prefs', 10 | component: PrefsComponent, 11 | // Mark this state as requiring authentication. See ../global/requiresAuth.hook.js. 12 | data: { requiresAuth: true } 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/util/README.md: -------------------------------------------------------------------------------- 1 | ## Contents 2 | 3 | ### Utility Code 4 | - *sessionStorage.js*: Provides an API that emulates a RESTful client API 5 | - *util.js*: various utility functions 6 | - *revertableModel*.js: Provides an API allowing an edited object to be reverted -------------------------------------------------------------------------------- /src/app/util/ga.ts: -------------------------------------------------------------------------------- 1 | import { sampleAppModuleAngularJS } from "../app.angularjs.module"; 2 | 3 | /** Google analytics */ 4 | 5 | ((function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 6 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*(new Date());a=s.createElement(o), 7 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 8 | }))(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 9 | 10 | window['ga']('create', 'UA-73329341-1', 'auto'); 11 | window['ga']('send', 'pageview'); 12 | 13 | sampleAppModuleAngularJS.config(googleAnalyticsConfigBlock); 14 | 15 | googleAnalyticsConfigBlock.$inject = ['$transitionsProvider']; 16 | function googleAnalyticsConfigBlock($transitionsProvider) { 17 | const vpv = (vpath) => 18 | window['ga']('send', 'pageview', vpath); 19 | 20 | const path = (trans) => { 21 | const formattedRoute = trans.$to().url.format(trans.params()); 22 | const withSitePrefix = location.pathname + formattedRoute; 23 | return `/${withSitePrefix.split('/').filter(x => x).join('/')}`; 24 | }; 25 | 26 | const error = (trans) => { 27 | const err = trans.error(); 28 | const type = err && err.hasOwnProperty('type') ? err.type : '_'; 29 | const message = err && err.hasOwnProperty('message') ? err.message : '_'; 30 | vpv(path(trans) + ';errorType=' + type + ';errorMessage=' + message); 31 | }; 32 | 33 | $transitionsProvider.onSuccess({}, (trans) => vpv(path(trans)), { priority: -10000 }); 34 | $transitionsProvider.onError({}, (trans) => error(trans), { priority: -10000 }); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/util/sessionStorage.ts: -------------------------------------------------------------------------------- 1 | import {pushToArr, guid, setProp} from "./util"; 2 | 3 | /** 4 | * This class simulates a RESTful resource, but the API calls fetch data from 5 | * Session Storage instead of an HTTP call. 6 | * 7 | * Once configured, it loads the initial (pristine) data from the URL provided (using HTTP). 8 | * It exposes GET/PUT/POST/DELETE-like API that operates on the data. All the data is also 9 | * stored in Session Storage. If any data is modified in memory, session storage is updated. 10 | * If the browser is refreshed, the SessionStorage object will try to fetch the existing data from 11 | * the session, before falling back to re-fetching the initial data using HTTP. 12 | * 13 | * For an example, please see dataSources.js 14 | */ 15 | export class SessionStorage { 16 | // data 17 | _data; 18 | _idProp; 19 | _eqFn; 20 | 21 | /** 22 | * Creates a new SessionStorage object 23 | * 24 | * @param $http Pass in the $http service 25 | * @param $timeout Pass in the $timeout service 26 | * @param $q Pass in the $q service 27 | * @param sessionStorageKey The session storage key. The data will be stored in browser's session storage under this key. 28 | * @param sourceUrl The url that contains the initial data. 29 | * @param AppConfig Pass in the AppConfig object 30 | */ 31 | constructor($http, public $timeout, public $q, public sessionStorageKey, sourceUrl, public AppConfig) { 32 | let data, fromSession = sessionStorage.getItem(sessionStorageKey); 33 | // A promise for *all* of the data. 34 | this._data = undefined; 35 | 36 | // For each data object, the _idProp defines which property has that object's unique identifier 37 | this._idProp = "_id"; 38 | 39 | // A basic triple-equals equality checker for two values 40 | this._eqFn = (l, r) => l[this._idProp] === r[this._idProp]; 41 | 42 | if (fromSession) { 43 | try { 44 | // Try to parse the existing data from the Session Storage API 45 | data = JSON.parse(fromSession); 46 | } catch (e) { 47 | console.log("Unable to parse session messages, retrieving intial data."); 48 | } 49 | } 50 | 51 | let stripHashKey = (obj) => 52 | setProp(obj, '$$hashKey', undefined); 53 | 54 | // Create a promise for the data; Either the existing data from session storage, or the initial data via $http request 55 | this._data = (data ? $q.resolve(data) : $http.get(sourceUrl).then(resp => resp.data)) 56 | .then(this._commit.bind(this)) 57 | .then(() => JSON.parse(sessionStorage.getItem(sessionStorageKey))) 58 | .then(array => array.map(stripHashKey)); 59 | 60 | } 61 | 62 | /** Saves all the data back to the session storage */ 63 | _commit(data) { 64 | sessionStorage.setItem(this.sessionStorageKey, JSON.stringify(data)); 65 | return this.$q.resolve(data); 66 | } 67 | 68 | /** Helper which simulates a delay, then provides the `thenFn` with the data */ 69 | all(thenFn) { 70 | return this.$timeout(() => this._data, this.AppConfig.restDelay).then(thenFn); 71 | } 72 | 73 | /** Given a sample item, returns a promise for all the data for items which have the same properties as the sample */ 74 | search(exampleItem) { 75 | let contains = (search, inString) => 76 | ("" + inString).indexOf("" + search) !== -1; 77 | let matchesExample = (example, item) => 78 | Object.keys(example).reduce((memo, key) => memo && contains(example[key], item[key]), true); 79 | return this.all(items => 80 | items.filter(matchesExample.bind(null, exampleItem))); 81 | } 82 | 83 | /** Returns a promise for the item with the given identifier */ 84 | get(id) { 85 | return this.all(items => 86 | items.find(item => item[this._idProp] === id)); 87 | } 88 | 89 | /** Returns a promise to save the item. It delegates to put() or post() if the object has or does not have an identifier set */ 90 | save(item) { 91 | return item[this._idProp] ? this.put(item) : this.post(item); 92 | } 93 | 94 | /** Returns a promise to save (POST) a new item. The item's identifier is auto-assigned. */ 95 | post(item) { 96 | item[this._idProp] = guid(); 97 | return this.all(items => pushToArr(items, item)).then(this._commit.bind(this)); 98 | } 99 | 100 | /** Returns a promise to save (PUT) an existing item. */ 101 | put(item, eqFn = this._eqFn) { 102 | return this.all(items => { 103 | let idx = items.findIndex(eqFn.bind(null, item)); 104 | if (idx === -1) throw Error(`${item} not found in ${this}`); 105 | items[idx] = item; 106 | return this._commit(items).then(() => item); 107 | }); 108 | } 109 | 110 | /** Returns a promise to remove (DELETE) an item. */ 111 | remove(item, eqFn = this._eqFn) { 112 | return this.all(items => { 113 | let idx = items.findIndex(eqFn.bind(null, item)); 114 | if (idx === -1) throw Error(`${item} not found in ${this}`); 115 | items.splice(idx, 1); 116 | return this._commit(items).then(() => item); 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/app/util/util.ts: -------------------------------------------------------------------------------- 1 | /** Some utility functions used by the application */ 2 | 3 | export const setProp = (obj, key, val) => { obj[key] = val; return obj; }; 4 | export const pushToArr = (array, item) => { array.push(item); return array; }; 5 | export const uniqReduce = (arr, item) => arr.indexOf(item) !== -1 ? arr : pushToArr(arr, item); 6 | export const flattenReduce = (arr, item) => arr.concat(item); 7 | let guidChar = (c) => c !== 'x' && c !== 'y' ? '-' : Math.floor(Math.random()*16).toString(16).toUpperCase(); 8 | export const guid = () => "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".split("").map(guidChar).join(""); 9 | -------------------------------------------------------------------------------- /src/data/README.md: -------------------------------------------------------------------------------- 1 | ## Contents 2 | 3 | This directory contains the fake data used by the fake REST services. 4 | 5 | ### Data 6 | - *contacts.json*: The users' contacts (currently they share the contact list) 7 | - *folders.json*: Folder list (and meta-data, such as columns to display for a folder) 8 | - *messages.json*: The users' messages 9 | - *corpora*: Directory containing markov chain seed corpora for generating styles of messages 10 | 11 | ### Scripts 12 | - *fetch.sh*: Fetches original contacts list and non-markov messages from json-generator.com 13 | - *generate.sh*: Fetches `jsmarkov` project and re-generates markov messages 14 | - *generate.js*: Driver code for `jsmarkov` 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/data/contacts.json: -------------------------------------------------------------------------------- 1 | [{"tags":[7,"consectetur"],"address":{"zip":42272,"state":"Washington","city":"Linganore","street":"394 Nova Court"},"phone":"+1 (856) 490-2330","email":"finley.ruiz@enersave.info","company":"ENERSAVE","age":33,"picture":"http://placebear.com/187/187","_id":"fruiz","name":{"last":"Ruiz","first":"Finley"}},{"tags":[7,"consectetur"],"address":{"zip":47608,"state":"Delaware","city":"Conestoga","street":"446 Cortelyou Road"},"phone":"+1 (864) 592-2641","email":"mitzi.kinney@zeam.net","company":"ZEAM","age":36,"picture":"http://placebear.com/149/250","_id":"mkinney","name":{"last":"Kinney","first":"Mitzi"}},{"tags":[7,"consectetur"],"address":{"zip":26182,"state":"Indiana","city":"Carrizo","street":"731 Brooklyn Road"},"phone":"+1 (984) 587-2537","email":"sallie.mann@namegen.ca","company":"NAMEGEN","age":20,"picture":"http://placebear.com/184/249","_id":"smann","name":{"last":"Mann","first":"Sallie"}},{"tags":[7,"consectetur"],"address":{"zip":13964,"state":"Georgia","city":"Cassel","street":"662 Bristol Street"},"phone":"+1 (822) 581-2476","email":"mckay.hatfield@idego.me","company":"IDEGO","age":29,"picture":"http://placebear.com/138/189","_id":"mhatfield","name":{"last":"Hatfield","first":"Mckay"}},{"tags":[7,"consectetur"],"address":{"zip":83300,"state":"Mississippi","city":"Newry","street":"945 Noble Street"},"phone":"+1 (956) 598-2570","email":"delacruz.avila@anixang.org","company":"ANIXANG","age":23,"picture":"http://placebear.com/152/236","_id":"davila","name":{"last":"Avila","first":"Delacruz"}},{"tags":[7,"consectetur"],"address":{"zip":59368,"state":"South Dakota","city":"Gulf","street":"825 Sapphire Street"},"phone":"+1 (810) 511-2092","email":"vonda.tran@corpulse.io","company":"CORPULSE","age":26,"picture":"http://placebear.com/137/213","_id":"vtran","name":{"last":"Tran","first":"Vonda"}},{"tags":[7,"consectetur"],"address":{"zip":14422,"state":"Oklahoma","city":"Breinigsville","street":"522 Court Square"},"phone":"+1 (939) 402-3205","email":"chandler.christensen@xth.co.uk","company":"XTH","age":24,"picture":"http://placebear.com/156/197","_id":"cchristensen","name":{"last":"Christensen","first":"Chandler"}},{"tags":[7,"consectetur"],"address":{"zip":22182,"state":"New Mexico","city":"Wyoming","street":"524 Gunnison Court"},"phone":"+1 (919) 508-2916","email":"turner.cameron@shadease.tv","company":"SHADEASE","age":31,"picture":"http://placebear.com/160/210","_id":"tcameron","name":{"last":"Cameron","first":"Turner"}},{"tags":[7,"consectetur"],"address":{"zip":66668,"state":"Iowa","city":"Duryea","street":"976 Beadel Street"},"phone":"+1 (989) 510-2305","email":"underwood.owens@entropix.us","company":"ENTROPIX","age":33,"picture":"http://placebear.com/152/217","_id":"uowens","name":{"last":"Owens","first":"Underwood"}},{"tags":[7,"consectetur"],"address":{"zip":53900,"state":"District Of Columbia","city":"Bison","street":"863 Fillmore Place"},"phone":"+1 (816) 519-2068","email":"rios.sears@telpod.biz","company":"TELPOD","age":30,"picture":"http://placebear.com/200/201","_id":"rsears","name":{"last":"Sears","first":"Rios"}},{"tags":[7,"consectetur"],"address":{"zip":37214,"state":"Connecticut","city":"Omar","street":"304 Fay Court"},"phone":"+1 (847) 505-2431","email":"rojas.combs@maroptic.biz","company":"MAROPTIC","age":31,"picture":"http://placebear.com/173/244","_id":"rcombs","name":{"last":"Combs","first":"Rojas"}},{"tags":[7,"consectetur"],"address":{"zip":34154,"state":"New Jersey","city":"Dale","street":"441 Judge Street"},"phone":"+1 (857) 571-3040","email":"ester.morgan@zolarity.name","company":"ZOLARITY","age":38,"picture":"http://placebear.com/160/230","_id":"emorgan","name":{"last":"Morgan","first":"Ester"}},{"tags":[7,"consectetur"],"address":{"zip":13228,"state":"West Virginia","city":"Trinway","street":"250 Dinsmore Place"},"phone":"+1 (907) 487-2234","email":"maribel.prince@tripsch.info","company":"TRIPSCH","age":31,"picture":"http://placebear.com/185/195","_id":"mprince","name":{"last":"Prince","first":"Maribel"}},{"tags":[7,"consectetur"],"address":{"zip":24366,"state":"Hawaii","city":"Moscow","street":"350 Homecrest Court"},"phone":"+1 (859) 548-3945","email":"concetta.higgins@mitroc.net","company":"MITROC","age":32,"picture":"http://placebear.com/164/217","_id":"chiggins","name":{"last":"Higgins","first":"Concetta"}},{"tags":[7,"consectetur"],"address":{"zip":26951,"state":"Puerto Rico","city":"Kent","street":"958 Granite Street"},"phone":"+1 (811) 591-3229","email":"delia.hunter@fortean.ca","company":"FORTEAN","age":27,"picture":"http://placebear.com/178/228","_id":"dhunter","name":{"last":"Hunter","first":"Delia"}},{"tags":[7,"consectetur"],"address":{"zip":91136,"state":"Illinois","city":"Kraemer","street":"225 Bushwick Court"},"phone":"+1 (965) 588-3336","email":"marlene.wilkinson@halap.me","company":"HALAP","age":38,"picture":"http://placebear.com/187/209","_id":"mwilkinson","name":{"last":"Wilkinson","first":"Marlene"}},{"tags":[7,"consectetur"],"address":{"zip":29582,"state":"Utah","city":"Edinburg","street":"563 Bartlett Place"},"phone":"+1 (994) 428-2015","email":"marsha.dillon@codax.org","company":"CODAX","age":20,"picture":"http://placebear.com/195/222","_id":"mdillon","name":{"last":"Dillon","first":"Marsha"}},{"tags":[7,"consectetur"],"address":{"zip":18460,"state":"New York","city":"Cornucopia","street":"351 Irvington Place"},"phone":"+1 (860) 416-3012","email":"amparo.cooke@permadyne.io","company":"PERMADYNE","age":24,"picture":"http://placebear.com/184/193","_id":"acooke","name":{"last":"Cooke","first":"Amparo"}},{"tags":[7,"consectetur"],"address":{"zip":56073,"state":"Missouri","city":"Guilford","street":"581 Canton Court"},"phone":"+1 (866) 578-3557","email":"stacey.oneal@netur.co.uk","company":"NETUR","age":27,"picture":"http://placebear.com/133/205","_id":"soneal","name":{"last":"Oneal","first":"Stacey"}},{"tags":[7,"consectetur"],"address":{"zip":49686,"state":"Marshall Islands","city":"Catharine","street":"432 Hart Place"},"phone":"+1 (875) 582-3295","email":"sharron.brennan@supremia.tv","company":"SUPREMIA","age":20,"picture":"http://placebear.com/154/209","_id":"sbrennan","name":{"last":"Brennan","first":"Sharron"}},{"tags":[7,"consectetur"],"address":{"zip":81298,"state":"Federated States Of Micronesia","city":"Columbus","street":"765 Keen Court"},"phone":"+1 (813) 420-2694","email":"christian.grimes@bezal.us","company":"BEZAL","age":23,"picture":"http://placebear.com/193/201","_id":"cgrimes","name":{"last":"Grimes","first":"Christian"}},{"tags":[7,"consectetur"],"address":{"zip":68785,"state":"Arkansas","city":"Warsaw","street":"124 Dooley Street"},"phone":"+1 (871) 496-2198","email":"johnnie.cardenas@ontality.biz","company":"ONTALITY","age":38,"picture":"http://placebear.com/172/208","_id":"jcardenas","name":{"last":"Cardenas","first":"Johnnie"}},{"tags":[7,"consectetur"],"address":{"zip":54344,"state":"Maryland","city":"Cedarville","street":"473 Utica Avenue"},"phone":"+1 (946) 593-2302","email":"henson.ware@zolarex.biz","company":"ZOLAREX","age":32,"picture":"http://placebear.com/131/247","_id":"hware","name":{"last":"Ware","first":"Henson"}},{"tags":[7,"consectetur"],"address":{"zip":13348,"state":"North Dakota","city":"Ellerslie","street":"393 Rock Street"},"phone":"+1 (817) 496-2776","email":"gilmore.alvarado@veraq.name","company":"VERAQ","age":27,"picture":"http://placebear.com/134/248","_id":"galvarado","name":{"last":"Alvarado","first":"Gilmore"}},{"tags":[7,"consectetur"],"address":{"zip":51906,"state":"Colorado","city":"Stevens","street":"739 Scholes Street"},"phone":"+1 (801) 569-3285","email":"salazar.craig@zilencio.info","company":"ZILENCIO","age":28,"picture":"http://placebear.com/179/182","_id":"scraig","name":{"last":"Craig","first":"Salazar"}}] -------------------------------------------------------------------------------- /src/data/corpora/2nd-treatise.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/2nd-treatise.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/beatles.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/beatles.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/beowulf.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/beowulf.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/bsdfaq.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/bsdfaq.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/cat-in-the-hat.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/cat-in-the-hat.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/comm_man.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/comm_man.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/elflore.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/elflore.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/flatland.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/flatland.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/green-eggs.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/green-eggs.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/macbeth.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/macbeth.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/palin.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/palin.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/rfc2549.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/rfc2549.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/rfc7230.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/rfc7230.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/sneetches.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/sneetches.txt.gz -------------------------------------------------------------------------------- /src/data/corpora/two-cities.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/data/corpora/two-cities.txt.gz -------------------------------------------------------------------------------- /src/data/fetch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | curl -o messages.txt http://beta.json-generator.com/api/json/get/VJl5GbIze 3 | curl -o contacts.json http://beta.json-generator.com/api/json/get/V1g6UwwGx 4 | -------------------------------------------------------------------------------- /src/data/folders.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "inbox", 4 | "columns": [ "read", "from", "subject", "date" ], 5 | "actions": [ "reply", "forward", "delete" ] 6 | }, 7 | { 8 | "_id": "finance", 9 | "columns": [ "read", "from", "subject", "date" ], 10 | "actions": [ "reply", "forward", "delete" ] 11 | }, 12 | { 13 | "_id": "travel", 14 | "columns": [ "read", "from", "subject", "date" ], 15 | "actions": [ "reply", "forward", "delete" ] 16 | }, 17 | { 18 | "_id": "personal", 19 | "columns": [ "read", "from", "subject", "date" ], 20 | "actions": [ "reply", "forward", "delete" ] 21 | }, 22 | { 23 | "_id": "spam", 24 | "columns": [ "read", "from", "subject", "date" ], 25 | "actions": [ "reply", "forward", "delete" ] 26 | }, 27 | { 28 | "_id": "drafts", 29 | "columns": [ "read", "to", "subject", "date" ], 30 | "actions": [ "edit", "delete" ] 31 | }, 32 | { 33 | "_id": "sent", 34 | "columns": [ "read", "to", "subject", "date" ], 35 | "actions": [ "forward", "delete" ] 36 | } 37 | ] -------------------------------------------------------------------------------- /src/data/generate.js: -------------------------------------------------------------------------------- 1 | var Markov = require('./markov').Markov; 2 | 3 | var zlib = require('zlib'), 4 | fs = require('fs'); 5 | var path = require('path'); 6 | 7 | 8 | var currentMarkov; 9 | var markovs = {}; 10 | var messages = JSON.parse(fs.readFileSync('messages.txt')); 11 | messages.forEach(function(message) { 12 | currentMarkov = getMarkov(message.corpus); 13 | message.subject = sen2gibberish(1, message.subject); 14 | var pars = message.body.split("\n"); 15 | var sig = pars.slice(-2); 16 | message.body = msg2gibberish(message.body) + '\n' + sig.join("\n"); 17 | }); 18 | fs.writeFile('messages.json', JSON.stringify(messages), 'utf-8'); 19 | 20 | 21 | function sen2gibberish(mult, sentence) { 22 | var words = sentence.split(/\s+/).length * mult; 23 | var start = currentMarkov.randomStart(); 24 | var gibberish = currentMarkov.generate(start, words); 25 | 26 | var sentence = gibberish; 27 | 28 | while (sentence.match(/^ *[,.?!]/)) { 29 | var words = sentence.split(/\s/); 30 | var newWord = currentMarkov.generate(words.slice(-2), 1); 31 | sentence = words.slice(1).join(" ") + newWord; 32 | } 33 | 34 | sentence = sentence.replace(/ ([^\w])/g, "$1"); 35 | 36 | sentence = sentence.replace(/[!?.] [a-z]/g, function (txt) { 37 | var len = txt.length; 38 | return txt.substr(0, len-1) + txt.charAt(len - 1).toUpperCase(); 39 | }); 40 | 41 | sentence = sentence.trim().replace(/\w.+/, function(txt){ 42 | return txt.charAt(0).toUpperCase() + txt.substr(1); 43 | }); 44 | 45 | console.log("sentence: " + sentence); 46 | return sentence + "."; 47 | } 48 | 49 | function par2gibberish(paragraph) { 50 | return paragraph.split(/\./) 51 | .map(sen2gibberish.bind(null, 2)) 52 | .join(" "); 53 | } 54 | 55 | function msg2gibberish(message) { 56 | return message.split("\n") 57 | .filter(function(s) { return s.trim().length; }) 58 | .map(par2gibberish) 59 | .join("\n"); 60 | } 61 | 62 | 63 | 64 | function getMarkov(corpus) { 65 | if (!markovs[corpus]) { 66 | markovs[corpus] = new Markov(); 67 | var inputFile = path.join(__dirname, '/corpora/', corpus + '.txt'); 68 | var text = fs.readFileSync(inputFile, 'utf8'); 69 | text = text.replace(/([^\s\w.,?!])/g, ""); 70 | text = text.replace(/([.,?!])/g, " $1 "); 71 | console.log("*** Training markov corpus with " + corpus); 72 | markovs[corpus].train(text); 73 | } 74 | return markovs[corpus]; 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/data/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./fetch.sh 3 | curl -O https://raw.githubusercontent.com/banksean/jsmarkov/master/markov.js 4 | curl -O https://raw.githubusercontent.com/banksean/jsmarkov/master/probabilityset.js 5 | 6 | gunzip -f -k corpora/*.gz 7 | 8 | cat probabilityset.js >> markov.js 9 | echo >> markov.js 10 | echo "exports.Markov = Markov;" >> markov.js 11 | 12 | node generate 13 | 14 | rm markov.js probabilityset.js 15 | rm corpora/*.txt 16 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ui-router/sample-app-angular-hybrid/b772ebdfea19d8cc6268a13312d6e0bce7cf575b/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularCliApp 6 | 7 | 8 | 9 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | (window as any).angular = angular; 3 | 4 | import { enableProdMode } from '@angular/core'; 5 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 6 | 7 | import { AppModule } from './app/app.module'; 8 | import { environment } from './environments/environment'; 9 | 10 | import { UIRouter, UrlService } from '@uirouter/core'; 11 | import { visualizer } from '@uirouter/visualizer'; 12 | import { sampleAppModuleAngularJS } from "./app/app.angularjs.module"; 13 | 14 | // Google analytics 15 | import './app/util/ga'; 16 | 17 | if (environment.production) { 18 | enableProdMode(); 19 | } 20 | 21 | /** 22 | * This file is the main entry point for the entire app. 23 | * 24 | * If the application is being bundled, this is where the bundling process 25 | * starts. If the application is being loaded by an es6 module loader, this 26 | * is the entry point. 27 | * 28 | * Point Webpack or SystemJS to this file. 29 | * 30 | * This module imports all the different parts of the application and registers them with angular. 31 | * - Submodules 32 | * - States 33 | * - Components 34 | * - Directives 35 | * - Services 36 | * - Filters 37 | * - Run and Config blocks 38 | * - Transition Hooks 39 | * - 3rd party Libraries and angular1 module 40 | * 41 | * Then this module creates the ng-upgrade adapter 42 | * and bootstraps the hybrid application 43 | */ 44 | 45 | 46 | 47 | 48 | // Using AngularJS config block, call `deferIntercept()`. 49 | // This tells UI-Router to delay the initial URL sync (until all bootstrapping is complete) 50 | sampleAppModuleAngularJS.config([ '$urlServiceProvider', ($urlService: UrlService) => $urlService.deferIntercept() ]); 51 | 52 | // Manually bootstrap the Angular app 53 | platformBrowserDynamic().bootstrapModule(AppModule).then(platformRef => { 54 | // Intialize the Angular Module 55 | // get() the UIRouter instance from DI to initialize the router 56 | const urlService: UrlService = platformRef.injector.get(UIRouter).urlService; 57 | 58 | // Instruct UIRouter to listen to URL changes 59 | urlService.listen(); 60 | urlService.sync(); 61 | }); 62 | 63 | // Show ui-router-visualizer 64 | sampleAppModuleAngularJS.run(['$uiRouter', ($uiRouter) => visualizer($uiRouter) ]); 65 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /*************************************************************************************************** 7 | * Zone JS is required by default for Angular itself. 8 | */ 9 | import 'zone.js'; // Included with Angular CLI. 10 | 11 | 12 | 13 | /*************************************************************************************************** 14 | * APPLICATION IMPORTS 15 | */ 16 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | button.btn i + span { 4 | margin-left: 0.75em; 5 | } 6 | 7 | .flex-h { 8 | display: flex; 9 | flex-flow: row wrap; 10 | } 11 | 12 | .flex-v { 13 | display: flex; 14 | flex-flow: column wrap; 15 | } 16 | 17 | .flex-h > * { 18 | flex: 1 1; 19 | } 20 | 21 | .flex.grow { 22 | flex: 1 0 auto; 23 | } 24 | 25 | .flex.nogrow { 26 | flex: 0 1 auto; 27 | } 28 | 29 | 30 | .home.buttons { 31 | display: flex; 32 | flex-flow: row wrap; 33 | justify-content: space-around; 34 | align-items: center; 35 | height: 45%; 36 | } 37 | 38 | .home.buttons button { 39 | padding: 2em; 40 | margin: 0; 41 | border: 3px solid black; 42 | border-radius: 1em; 43 | text-shadow: 0.1em 0.1em 0.1em black; 44 | } 45 | 46 | .home.buttons button i { 47 | padding: 0; 48 | margin: 0; 49 | text-shadow: 0.1em 0.1em 0.1em black; 50 | } 51 | 52 | .navheader { 53 | margin: 0.3em 0.3em 0 0.3em; 54 | } 55 | 56 | .navheader .nav-tabs li.active a:focus { 57 | background-color: #f7f7f7; 58 | } 59 | 60 | .navheader .nav-tabs li.active a { 61 | border-color: lightgray lightgray rgba(0,0,0,0) lightgray ; 62 | background-color: #f7f7f7; 63 | } 64 | 65 | .navheader .nav-tabs li a { 66 | border-width: 1px; 67 | border-radius: 10px 10px 0 0; 68 | } 69 | 70 | .navheader .logged-in-user div.hoverdrop { 71 | display: none; 72 | position: absolute; 73 | width: 100%; 74 | } 75 | 76 | .navheader .logged-in-user:hover div.hoverdrop { 77 | display: flex; 78 | flex-direction:row-reverse; 79 | } 80 | 81 | /* my messages */ 82 | 83 | .my-messages { 84 | width: 100%; 85 | display: flex; 86 | max-height: 200px; 87 | } 88 | 89 | .my-messages .folderlist { 90 | flex: 0 0 auto; 91 | overflow-y: scroll; 92 | max-height: 200px; 93 | } 94 | 95 | 96 | 97 | 98 | /* selection lists */ 99 | .selectlist { 100 | margin: 0; 101 | padding: 1.5em 0; 102 | background-color: #DDD; 103 | } 104 | .selectlist a { 105 | display: block; 106 | color: black; 107 | padding: 0.15em 1.5em; 108 | text-decoration: none; 109 | font-size: small; 110 | } 111 | .selectlist .selected { 112 | background-color: cornflowerblue; 113 | } 114 | .selectlist .selected a { 115 | color: white; 116 | } 117 | .selectlist i.fa { 118 | width: 1.35em; 119 | } 120 | 121 | 122 | 123 | /* folder list */ 124 | .selectlist .folder.selected i.fa:before { 125 | content: "\f115"; 126 | } 127 | .selectlist .folder:not(.selected) i.fa:before { 128 | content: "\f114"; 129 | } 130 | 131 | 132 | 133 | .ellipsis { 134 | text-overflow: ellipsis; 135 | } 136 | 137 | 138 | 139 | 140 | /* message list */ 141 | 142 | .messagelist { 143 | flex: 1 0 12em; 144 | overflow-y: scroll; 145 | max-height: 200px; 146 | -webkit-user-select: none; 147 | -moz-user-select: none; 148 | -ms-user-select: none; 149 | user-select: none; 150 | } 151 | 152 | .messagelist table { 153 | table-layout: fixed; 154 | width:100%; 155 | } 156 | 157 | .messagelist thead td { 158 | font-weight: bold; 159 | } 160 | 161 | .messagelist thead td.st-sort-ascent:after { 162 | content: " \f0de"; 163 | font-family: "FontAwesome"; 164 | } 165 | 166 | .messagelist thead td.st-sort-descent:after { 167 | content: " \f0dd"; 168 | font-family: "FontAwesome"; 169 | } 170 | 171 | .messagelist table td { 172 | padding: 0.25em 0.75em; 173 | white-space: nowrap; 174 | overflow: hidden; 175 | text-overflow: ellipsis; 176 | font-size: small; 177 | cursor: default; 178 | } 179 | 180 | .messagelist i.fa-circle { 181 | color: cornflowerblue; 182 | font-size: 50%; 183 | } 184 | 185 | .messagelist table tr.active { 186 | background-color: cornflowerblue; 187 | color: white; 188 | } 189 | 190 | .messagelist table td:nth-child(1){ width: 1.75em; } 191 | .messagelist table td:nth-child(2){ width: 21%; } 192 | .messagelist table td:nth-child(3){ width: 62%; } 193 | .messagelist table td:nth-child(4){ width: 15%; } 194 | 195 | .messagelist thead tr:first-child { 196 | /*position:absolute;*/ 197 | } 198 | 199 | .messagelist tbody tr:first-child td{ 200 | /*padding-top:28px;*/ 201 | } 202 | 203 | 204 | 205 | 206 | /* message content */ 207 | 208 | .message .header,.body { 209 | padding: 1em 2em; 210 | } 211 | 212 | .message .header { 213 | background-color: darkgray; 214 | display: flex; 215 | flex-flow: row wrap; 216 | justify-content: space-between; 217 | align-items: baseline; 218 | } 219 | 220 | 221 | .message .header .line2 { 222 | display: flex; 223 | flex-flow: column; 224 | justify-content: space-between; 225 | } 226 | 227 | 228 | .compose .header label { 229 | padding: 0 1em; 230 | flex: 0 0 6em; 231 | } 232 | 233 | .compose .body { 234 | background-color: lightgray; 235 | height: 100%; 236 | } 237 | .compose .body textarea { 238 | width: 100%; 239 | border: 0; 240 | outline: 0; 241 | } 242 | 243 | .compose .body .buttons { 244 | padding: 1em 0; 245 | float: right; 246 | } 247 | 248 | 249 | 250 | 251 | 252 | .contact { 253 | padding: 1em 3em; 254 | } 255 | 256 | .contact .details > div { 257 | display: flex; 258 | flex-flow: row wrap; 259 | } 260 | 261 | .contact .details h3 { 262 | margin-top: 0; 263 | } 264 | 265 | .contact .details label { 266 | width: 8em; 267 | flex: 0 1 auto; 268 | } 269 | 270 | .contact .details input { 271 | flex: 1 1 auto; 272 | } 273 | 274 | 275 | 276 | 277 | 278 | .dialog { 279 | position: fixed; 280 | top: 0; 281 | right: 0; 282 | bottom: 0; 283 | left: 0; 284 | z-index: 1040; 285 | } 286 | 287 | .dialog .backdrop { 288 | position: fixed; 289 | top: 0; 290 | right: 0; 291 | bottom: 0; 292 | left: 0; 293 | transition: opacity 0.25s ease; 294 | background: #000; 295 | opacity: 0; 296 | } 297 | 298 | .dialog.active .backdrop { 299 | opacity: 0.5; 300 | } 301 | 302 | .dialog .wrapper { 303 | z-index: 1041; 304 | opacity: 1; 305 | display: flex; 306 | flex-direction: column; 307 | align-items: center; 308 | } 309 | 310 | .dialog .content { 311 | transition: top 0.25s ease; 312 | flex: 1 1 auto; 313 | background: white; 314 | padding: 2em; 315 | position: relative; 316 | top: -200px; 317 | } 318 | 319 | .dialog.active .content { 320 | top: 4em; 321 | } 322 | 323 | 324 | .bounce-horizontal { 325 | animation: bounce-horizontal 0.5s alternate infinite; 326 | } 327 | 328 | @keyframes bounce-horizontal { 329 | 0% { left: 1.5em; } 330 | 100% { left: 0.5em; } 331 | } 332 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), { 14 | teardown: { destroyAfterEach: false } 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "main.ts", 10 | "polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "downlevelIteration": true, 5 | "importHelpers": true, 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "target": "ES2022", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ], 19 | "module": "es2020", 20 | "baseUrl": "./", 21 | "useDefineForClassFields": false 22 | } 23 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [], 3 | "rules": { 4 | "arrow-return-shorthand": true, 5 | "callable-types": true, 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "deprecation": { 13 | "severity": "warn" 14 | }, 15 | "eofline": true, 16 | "forin": true, 17 | "import-blacklist": [ 18 | true, 19 | "rxjs/Rx" 20 | ], 21 | "import-spacing": true, 22 | "indent": [ 23 | true, 24 | "spaces" 25 | ], 26 | "interface-over-type-literal": true, 27 | "label-position": true, 28 | "max-line-length": [ 29 | true, 30 | 140 31 | ], 32 | "member-access": false, 33 | "member-ordering": [ 34 | true, 35 | { 36 | "order": [ 37 | "static-field", 38 | "instance-field", 39 | "static-method", 40 | "instance-method" 41 | ] 42 | } 43 | ], 44 | "no-arg": true, 45 | "no-bitwise": true, 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-construct": true, 55 | "no-debugger": true, 56 | "no-duplicate-super": true, 57 | "no-empty": false, 58 | "no-empty-interface": true, 59 | "no-eval": true, 60 | "no-inferrable-types": [ 61 | true, 62 | "ignore-params" 63 | ], 64 | "no-misused-new": true, 65 | "no-non-null-assertion": true, 66 | "no-shadowed-variable": true, 67 | "no-string-literal": false, 68 | "no-string-throw": true, 69 | "no-switch-case-fall-through": true, 70 | "no-trailing-whitespace": true, 71 | "no-unnecessary-initializer": true, 72 | "no-unused-expression": true, 73 | "no-var-keyword": true, 74 | "object-literal-sort-keys": false, 75 | "one-line": [ 76 | true, 77 | "check-open-brace", 78 | "check-catch", 79 | "check-else", 80 | "check-whitespace" 81 | ], 82 | "prefer-const": true, 83 | "quotemark": [ 84 | true, 85 | "single" 86 | ], 87 | "radix": true, 88 | "semicolon": [ 89 | true, 90 | "always" 91 | ], 92 | "triple-equals": [ 93 | true, 94 | "allow-null-check" 95 | ], 96 | "typedef-whitespace": [ 97 | true, 98 | { 99 | "call-signature": "nospace", 100 | "index-signature": "nospace", 101 | "parameter": "nospace", 102 | "property-declaration": "nospace", 103 | "variable-declaration": "nospace" 104 | } 105 | ], 106 | "unified-signatures": true, 107 | "variable-name": false, 108 | "whitespace": [ 109 | true, 110 | "check-branch", 111 | "check-decl", 112 | "check-operator", 113 | "check-separator", 114 | "check-type" 115 | ], 116 | "directive-selector": [ 117 | true, 118 | "attribute", 119 | "app", 120 | "camelCase" 121 | ], 122 | "component-selector": [ 123 | true, 124 | "element", 125 | "app", 126 | "kebab-case" 127 | ], 128 | "no-output-on-prefix": true, 129 | "no-inputs-metadata-property": true, 130 | "no-outputs-metadata-property": true, 131 | "no-host-metadata-property": true, 132 | "no-input-rename": true, 133 | "no-output-rename": true, 134 | "use-lifecycle-interface": true, 135 | "use-pipe-transform-interface": true, 136 | "component-class-suffix": true, 137 | "directive-class-suffix": true 138 | } 139 | } 140 | --------------------------------------------------------------------------------