├── .editorconfig
├── .github
└── workflows
│ ├── ci.yml
│ └── update_dependencies.yml
├── .gitignore
├── .mergify.yml
├── .npmrc
├── README.md
├── angular.json
├── cypress.config.ts
├── cypress
├── e2e
│ └── sample_app.cy.js
├── fixtures
│ └── example.json
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ └── e2e.js
├── dependencies.yml
├── karma.conf.js
├── package.json
├── protractor.conf.js
├── src
├── app
│ ├── README.md
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── app.states.ts
│ ├── contacts
│ │ ├── README.md
│ │ ├── contact-detail.component.ts
│ │ ├── contact-list.component.ts
│ │ ├── contact.component.ts
│ │ ├── contacts-data.service.ts
│ │ ├── contacts.component.ts
│ │ ├── contacts.module.ts
│ │ ├── contacts.states.ts
│ │ └── edit-contact.component.ts
│ ├── global
│ │ ├── README.md
│ │ ├── app-config.service.ts
│ │ ├── auth.hook.ts
│ │ ├── auth.service.ts
│ │ ├── dialog.component.ts
│ │ ├── dialog.service.ts
│ │ └── global.module.ts
│ ├── home.component.ts
│ ├── login.component.ts
│ ├── main.component.ts
│ ├── mymessages
│ │ ├── compose.component.ts
│ │ ├── folder-list.component.ts
│ │ ├── folders-data.service.ts
│ │ ├── format-message.pipe.ts
│ │ ├── interface.ts
│ │ ├── message-list.component.ts
│ │ ├── message-table.component.ts
│ │ ├── message.component.ts
│ │ ├── messages-data.service.ts
│ │ ├── mymessages.component.ts
│ │ ├── mymessages.module.ts
│ │ ├── mymessages.states.ts
│ │ └── sort-messages.component.ts
│ ├── prefs
│ │ ├── README.md
│ │ ├── prefs.component.ts
│ │ ├── prefs.module.ts
│ │ └── prefs.states.ts
│ ├── router.config.ts
│ ├── util
│ │ ├── README.md
│ │ ├── ga.ts
│ │ ├── sessionStorage.ts
│ │ └── util.ts
│ └── welcome.component.ts
├── assets
│ ├── .gitkeep
│ ├── 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.json
├── 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: Check Peer Dependencies
32 | # run: npx check-peer-dependencies
33 | - name: Run Tests
34 | run: yarn ${{ matrix.yarncmd }}
35 |
--------------------------------------------------------------------------------
/.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: ['typescript,core-js']
16 | deptype: ['dependencies', 'devDependencies']
17 | latest: [true]
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 | });
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 | yarn-error.log
3 |
4 | # compiled output
5 | /dist
6 | /tmp
7 | .*
8 |
9 | # dependencies
10 | /node_modules
11 |
12 | # IDEs and editors
13 | *.launch
14 |
15 | # IDE - VSCode
16 | .vscode/*
17 | !.vscode/settings.json
18 | !.vscode/tasks.json
19 | !.vscode/launch.json
20 | !.vscode/extensions.json
21 |
22 | # misc
23 | /.angular/cache
24 | /connect.lock
25 | /coverage/*
26 | /libpeerconnection.log
27 | npm-debug.log
28 | testem.log
29 | /typings
30 |
31 | # e2e
32 | /e2e/*.js
33 | /e2e/*.map
34 |
35 | #System Files
36 | Thumbs.db
37 |
--------------------------------------------------------------------------------
/.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
28 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | scripts-prepend-node-path=true
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## UI-Router for Angular - Sample Application
2 |
3 | [](https://travis-ci.org/ui-router/sample-app-angular)
4 | [](https://greenkeeper.io/)
5 |
6 | This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.30.
7 |
8 | Live demo: http://ui-router.github.io/sample-app-angular/#/mymessages/inbox/5648b50cc586cac4aed6836f
9 |
10 | Edit this project in your browser: https://stackblitz.com/github/ui-router/sample-app-angular
11 |
12 | This sample app is intended to demonstrate a non-trivial ui-router application.
13 |
14 | - Multiple sub-modules
15 | - Managed state lifecycle
16 | - Application data lifecycle
17 | - Authentication (simulated)
18 | - Authenticated and unauthenticated states
19 | - REST data retrieval (simulated)
20 | - Lazy loaded Angular modules (contacts/mymessages/prefs submodules)
21 |
22 | ---
23 |
24 | ### Visualizer
25 |
26 | We're using the [State and Transition Visualizer](http://github.com/ui-router/visualizer) to visually represent
27 | the current state tree, as well as the transitions between states.
28 | Explore how transitions work by hovering over them, and clicking to expand details (params and resolves).
29 |
30 | Note how states are _entered_ when they were previously not active, _exited_ and re-_entered_ when parameters change,
31 | and how parent states whose parameters did not change are _retained_.
32 | Each of these (_exited, entered, retained_) correspond to a Transition Hook.
33 |
34 | ### Structure
35 |
36 | There are many ways to structure a ui-router app.
37 | We aren't super opinionated on application structure.
38 | Use what works for you.
39 | We organized ours in the following way:
40 |
41 | - Feature modules
42 | - Each feature gets its own directory and Angular Module (`@NgModule`)
43 | - Features contain states and components
44 | - Specific types of helper code (directives, services, etc) _used only within a feature_ may live in a subdirectory
45 | named after its type
46 | - Leveraging ES6 modules
47 | - States for a module are defined in separate file
48 | - Each component is defined in its own file
49 | - Components are referenced in states where they are composed into the state definition
50 | - States export themselves
51 | - Each feature `@NgModule` imports and declares all the states for the feature
52 |
53 | ### UI-Router Patterns
54 |
55 | - Defining custom, app-specific global behaviors
56 | - Add metadata to a state, or state tree (such as `authRequired`)
57 | - Check for metadata in transition hooks
58 | - Example: `routerhooks/authRequired.js`
59 | - If a transition to a state with a truthy `data.authRequired: true` property is started and the user is not currently authenticated
60 | - Defining a default substate for a top-level state
61 | - Example: declaring `redirectTo: 'welcome'` in `app.states.ts`
62 | - Defining a default parameter for a state
63 | - Example: `folderId` parameter defaults to 'inbox' in `mymessages.states.ts` (folder state)
64 | - Application data lifecycle
65 | - Data loading is managed by the state declaration, via the `resolve:` block
66 | - Data is fetched before the state is _entered_
67 | - Data is fetched according to state parameters
68 | - The state is _entered_ when the data is ready
69 | - The resolved data is injected into the components
70 | - The resolve data remains loaded until the state is exited
71 | - Lazy Loaded states
72 | - The main submodules (and all their states and components) are lazy loaded
73 | - The future state includes a `loadChildren` property which is used to lazy load the module
74 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "sample-app-angular": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "architect": {
11 | "build": {
12 | "builder": "@angular-devkit/build-angular:application",
13 | "options": {
14 | "outputPath": {
15 | "base": "dist"
16 | },
17 | "index": "src/index.html",
18 | "tsConfig": "src/tsconfig.json",
19 | "polyfills": [
20 | "src/polyfills.ts"
21 | ],
22 | "assets": [
23 | "src/assets",
24 | "src/favicon.ico"
25 | ],
26 | "styles": [
27 | "src/styles.css"
28 | ],
29 | "scripts": [],
30 | "extractLicenses": false,
31 | "sourceMap": true,
32 | "optimization": false,
33 | "namedChunks": true,
34 | "browser": "src/main.ts"
35 | },
36 | "configurations": {
37 | "production": {
38 | "budgets": [
39 | {
40 | "type": "anyComponentStyle",
41 | "maximumWarning": "6kb"
42 | }
43 | ],
44 | "optimization": true,
45 | "outputHashing": "all",
46 | "sourceMap": false,
47 | "namedChunks": false,
48 | "extractLicenses": true,
49 | "fileReplacements": [
50 | {
51 | "replace": "src/environments/environment.ts",
52 | "with": "src/environments/environment.prod.ts"
53 | }
54 | ]
55 | }
56 | },
57 | "defaultConfiguration": ""
58 | },
59 | "serve": {
60 | "builder": "@angular-devkit/build-angular:dev-server",
61 | "options": {
62 | "buildTarget": "sample-app-angular:build"
63 | },
64 | "configurations": {
65 | "production": {
66 | "buildTarget": "sample-app-angular:build:production"
67 | }
68 | }
69 | },
70 | "extract-i18n": {
71 | "builder": "@angular-devkit/build-angular:extract-i18n",
72 | "options": {
73 | "buildTarget": "sample-app-angular:build"
74 | }
75 | },
76 | "test": {
77 | "builder": "@angular-devkit/build-angular:karma",
78 | "options": {
79 | "main": "src/test.ts",
80 | "karmaConfig": "./karma.conf.js",
81 | "polyfills": "src/polyfills.ts",
82 | "scripts": [],
83 | "styles": [
84 | "src/styles.css"
85 | ],
86 | "assets": [
87 | "src/assets",
88 | "src/favicon.ico"
89 | ]
90 | }
91 | }
92 | }
93 | },
94 | "sample-app-angular-e2e": {
95 | "root": "",
96 | "sourceRoot": "",
97 | "projectType": "application",
98 | "architect": {
99 | "e2e": {
100 | "builder": "@angular-devkit/build-angular:protractor",
101 | "options": {
102 | "protractorConfig": "./protractor.conf.js",
103 | "devServerTarget": "sample-app-angular:serve"
104 | }
105 | }
106 | }
107 | }
108 | },
109 | "schematics": {
110 | "@schematics/angular:component": {
111 | "prefix": "app",
112 | "style": "css"
113 | },
114 | "@schematics/angular:directive": {
115 | "prefix": "app"
116 | }
117 | },
118 | "cli": {
119 | "analytics": false
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress'
2 |
3 | export default defineConfig({
4 | video: false,
5 | e2e: {
6 | // We've imported your old cypress plugins here.
7 | // You may want to clean this up later by importing these.
8 | setupNodeEvents(on, config) {
9 | return require('./cypress/plugins/index.js')(on, config)
10 | },
11 | baseUrl: 'http://localhost:4000',
12 | },
13 | })
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | module.exports = (on, config) => {
15 | // `on` is used to hook into various events Cypress emits
16 | // `config` is the resolved Cypress config
17 | }
18 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This is will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/cypress/support/e2e.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/dependencies.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | dependencies:
3 | - type: js
4 | settings:
5 | commit_message_prefix: "chore(package): "
6 | related_pr_behavior: close
7 | github_labels:
8 | - dependencies
9 | manifest_updates:
10 | filters:
11 | - name: "typescript"
12 | versions: L.L.Y
13 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/0.13/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-remap-istanbul'),
12 | require('@angular-devkit/build-angular/plugins/karma')
13 | ],
14 | files: [
15 |
16 | ],
17 | preprocessors: {
18 |
19 | },
20 | mime: {
21 | 'text/x-typescript': ['ts','tsx']
22 | },
23 | remapIstanbulReporter: {
24 | dir: require('path').join(__dirname, 'coverage'), reports: {
25 | html: 'coverage',
26 | lcovonly: './coverage/coverage.lcov'
27 | }
28 | },
29 | angularCli: {
30 | config: './angular-cli.json',
31 | environment: 'dev'
32 | },
33 | reporters: config.angularCli && config.angularCli.codeCoverage
34 | ? ['progress', 'karma-remap-istanbul']
35 | : ['progress'],
36 | port: 9876,
37 | colors: true,
38 | logLevel: config.LOG_INFO,
39 | autoWatch: true,
40 | browsers: ['Chrome'],
41 | singleRun: false
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sample-app-angular",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "angular-cli": {},
6 | "checkPeerDependencies": {
7 | "ignore": [
8 | "ajv",
9 | "postcss",
10 | "terser"
11 | ]
12 | },
13 | "scripts": {
14 | "ng": "ng",
15 | "start": "ng serve --configuration production",
16 | "build": "ng build --configuration production",
17 | "test": "npm run build && cypress-runner run --path=dist/browser",
18 | "test:open": "npm run build && cypress-runner open --path=dist/browser",
19 | "e2e": "npm run test",
20 | "gh-pages": "ng build --base-href=/sample-app-angular/ && shx rm -rf pages && shx mkdir pages && cd pages && git init && git remote add pages git@github.com:ui-router/sample-app-angular.git && git fetch pages && git checkout gh-pages && git rm -rf * && shx mv ../dist/browser/* . && git add . && git commit -m 'Update gh-pages' . && git push && cd .. && shx rm -rf pages"
21 | },
22 | "private": true,
23 | "dependencies": {
24 | "@angular/common": "^19.0.5",
25 | "@angular/compiler": "^19.0.5",
26 | "@angular/core": "^19.0.5",
27 | "@angular/forms": "^19.0.5",
28 | "@angular/platform-browser": "^19.0.5",
29 | "@angular/platform-browser-dynamic": "^19.0.5",
30 | "@uirouter/angular": "^14.0.0",
31 | "@uirouter/core": "6.1.0",
32 | "@uirouter/rx": "1.0.0",
33 | "@uirouter/visualizer": "^7.2.1",
34 | "rxjs": "^7.4.0",
35 | "zone.js": "~0.15.0"
36 | },
37 | "devDependencies": {
38 | "@angular-devkit/build-angular": "^19.0.6",
39 | "@angular/animations": "^19.0.5",
40 | "@angular/cli": "^19.0.6",
41 | "@angular/compiler-cli": "^19.0.5",
42 | "@types/jasmine": "~3.10.2",
43 | "@uirouter/cypress-runner": "^3.0.0",
44 | "tslint": "6.1.3",
45 | "typescript": "~5.6.3"
46 | },
47 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
48 | }
49 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | /*global jasmine */
5 | var SpecReporter = require('jasmine-spec-reporter');
6 |
7 | exports.config = {
8 | allScriptsTimeout: 11000,
9 | specs: [
10 | './e2e/**/*.e2e-spec.ts'
11 | ],
12 | capabilities: {
13 | 'browserName': 'chrome'
14 | },
15 | directConnect: true,
16 | baseUrl: 'http://localhost:4200/',
17 | framework: 'jasmine',
18 | jasmineNodeOpts: {
19 | showColors: true,
20 | defaultTimeoutInterval: 30000,
21 | print: function() {}
22 | },
23 | useAllAngular2AppRoots: true,
24 | beforeLaunch: function() {
25 | require('ts-node').register({
26 | project: 'e2e'
27 | });
28 | },
29 | onPrepare: function() {
30 | jasmine.getEnv().addReporter(new SpecReporter());
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/src/app/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/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild, ViewContainerRef, OnInit } from '@angular/core';
2 | import { DialogService } from './global/dialog.service';
3 | import { StateService } from '@uirouter/core';
4 | import { AuthService } from './global/auth.service';
5 | import { AppConfigService } from './global/app-config.service';
6 |
7 | /**
8 | * This is the main app component for an authenticated user.
9 | *
10 | * This component renders the outermost chrome
11 | * (application header and tabs, the compose and logout button)
12 | * It has a `ui-view` viewport for nested states to fill in.
13 | */
14 | @Component({
15 | selector: 'app-root',
16 | template: `
17 |
18 |
43 |
44 |
45 | `,
46 | styles: [],
47 | standalone: false
48 | })
49 | export class AppComponent implements OnInit {
50 | @ViewChild('dialogdiv', { read: ViewContainerRef, static: true }) dialogdiv;
51 |
52 | // data
53 | emailAddress;
54 | isAuthenticated;
55 |
56 | constructor(appConfig: AppConfigService,
57 | public authService: AuthService,
58 | public $state: StateService,
59 | private dialog: DialogService
60 | ) {
61 | this.emailAddress = appConfig.emailAddress;
62 | this.isAuthenticated = authService.isAuthenticated();
63 | }
64 |
65 | ngOnInit() {
66 | this.dialog.vcRef = this.dialogdiv;
67 | }
68 |
69 | show() {
70 | this.dialog.confirm('foo');
71 | }
72 |
73 | logout() {
74 | const { authService, $state } = this;
75 | authService.logout();
76 | // Reload states after authentication change
77 | return $state.go('welcome', {}, { reload: true });
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 | import { FormsModule } from '@angular/forms';
4 |
5 | import { MainComponent } from './main.component';
6 | import { AppComponent } from './app.component';
7 | import { WelcomeComponent } from './welcome.component';
8 | import { LoginComponent } from './login.component';
9 | import { HomeComponent } from './home.component';
10 | import { UIRouterModule } from '@uirouter/angular';
11 | import { APP_STATES } from './app.states';
12 | import { GlobalModule } from './global/global.module';
13 | import { routerConfigFn } from './router.config';
14 |
15 | @NgModule({
16 | declarations: [
17 | MainComponent,
18 | AppComponent,
19 | WelcomeComponent,
20 | LoginComponent,
21 | HomeComponent
22 | ],
23 | imports: [
24 | UIRouterModule.forRoot({
25 | states: APP_STATES,
26 | useHash: true,
27 | initial: { state: 'home' },
28 | config: routerConfigFn,
29 | }),
30 | GlobalModule,
31 | BrowserModule,
32 | FormsModule
33 | ],
34 | bootstrap: [MainComponent]
35 | })
36 | export class AppModule { }
37 |
--------------------------------------------------------------------------------
/src/app/app.states.ts:
--------------------------------------------------------------------------------
1 | import { AppComponent } from './app.component';
2 | import { WelcomeComponent } from './welcome.component';
3 | import { HomeComponent } from './home.component';
4 | import { LoginComponent } from './login.component';
5 | import { Transition } from '@uirouter/core';
6 |
7 | /**
8 | * This is the parent state for the entire application.
9 | *
10 | * This state's primary purposes are:
11 | * 1) Shows the outermost chrome (including the navigation and logout for authenticated users)
12 | * 2) Provide a viewport (ui-view) for a substate to plug into
13 | */
14 | export const appState = {
15 | name: 'app',
16 | redirectTo: 'welcome',
17 | component: AppComponent,
18 | };
19 |
20 | /**
21 | * This is the 'welcome' state. It is the default state (as defined by app.js) if no other state
22 | * can be matched to the URL.
23 | */
24 | export const welcomeState = {
25 | parent: 'app',
26 | name: 'welcome',
27 | url: '/welcome',
28 | component: WelcomeComponent,
29 | };
30 |
31 | /**
32 | * This is a home screen for authenticated users.
33 | *
34 | * It shows giant buttons which activate their respective submodules: Messages, Contacts, Preferences
35 | */
36 | export const homeState = {
37 | parent: 'app',
38 | name: 'home',
39 | url: '/home',
40 | component: HomeComponent,
41 | };
42 |
43 |
44 | /**
45 | * This is the login state. It is activated when the user navigates to /login, or if a unauthenticated
46 | * user attempts to access a protected state (or substate) which requires authentication. (see routerhooks/requiresAuth.js)
47 | *
48 | * It shows a fake login dialog and prompts the user to authenticate. Once the user authenticates, it then
49 | * reactivates the state that the user originally came from.
50 | */
51 | export const loginState = {
52 | parent: 'app',
53 | name: 'login',
54 | url: '/login',
55 | component: LoginComponent,
56 | resolve: [
57 | { token: 'returnTo', deps: [Transition], resolveFn: returnTo },
58 | ]
59 | };
60 |
61 | /**
62 | * A resolve function for 'login' state which figures out what state to return to, after a successful login.
63 | *
64 | * If the user was initially redirected to login state (due to the requiresAuth redirect), then return the toState/params
65 | * they were redirected from. Otherwise, if they transitioned directly, return the fromState/params. Otherwise
66 | * return the main "home" state.
67 | */
68 | export function returnTo ($transition$: Transition): any {
69 | if ($transition$.redirectedFrom() != null) {
70 | // The user was redirected to the login state (e.g., via the requiresAuth hook when trying to activate contacts)
71 | // Return to the original attempted target state (e.g., contacts)
72 | return $transition$.redirectedFrom().targetState();
73 | }
74 |
75 | const $state = $transition$.router.stateService;
76 |
77 | // The user was not redirected to the login state; they directly activated the login state somehow.
78 | // Return them to the state they came from.
79 | if ($transition$.from().name !== '') {
80 | return $state.target($transition$.from(), $transition$.params('from'));
81 | }
82 |
83 | // If the fromState's name is empty, then this was the initial transition. Just return them to the home state
84 | return $state.target('home');
85 | }
86 |
87 | // This future state is a placeholder for all the lazy loaded Contacts states
88 | // The Contacts NgModule isn't loaded until a Contacts link is activated
89 | export const contactsFutureState = {
90 | name: 'contacts.**',
91 | url: '/contacts',
92 | loadChildren: () => import('./contacts/contacts.module').then(m => m.ContactsModule)
93 | };
94 |
95 | // This future state is a placeholder for the lazy loaded Prefs states
96 | export const prefsFutureState = {
97 | name: 'prefs.**',
98 | url: '/prefs',
99 | loadChildren: () => import('./prefs/prefs.module').then(m => m.PrefsModule)
100 | };
101 |
102 | // This future state is a placeholder for the lazy loaded My Messages feature module
103 | export const mymessagesFutureState = {
104 | name: 'mymessages.**',
105 | url: '/mymessages',
106 | loadChildren: () => import('./mymessages/mymessages.module').then(m => m.MymessagesModule)
107 | };
108 |
109 | export const APP_STATES = [
110 | appState,
111 | welcomeState,
112 | homeState,
113 | loginState,
114 | contactsFutureState,
115 | prefsFutureState,
116 | mymessagesFutureState,
117 | ];
118 |
--------------------------------------------------------------------------------
/src/app/contacts/README.md:
--------------------------------------------------------------------------------
1 | ### The NgModule
2 | - *contacts.module*.ts: A Contacts feature module (NgModule)
3 |
4 | ## NgModule Contents
5 |
6 | ### The Contacts submodule states
7 |
8 | - *contacts.states*.ts: Defines the Contacts ui-router states
9 |
10 | ### The Contacts submodule components
11 |
12 | - *contact-detail.component*.ts: A component which renders a read only view of the details for a single contact.
13 | - *contact-list.component*.ts: A component which renders a list of contacts
14 | - *contacts.component*.ts: A component which renders the contacts submodule.
15 | - *contact.component*.ts: A component which renders details and controls for a single contact
16 | - *edit-contact.component*.ts: A component which edits a single contact.
17 |
18 | ### The Contacts fake REST API
19 |
20 | - *contacts.service*.ts: A fake REST API which can GET/POST contact(s)
21 |
--------------------------------------------------------------------------------
/src/app/contacts/contact-detail.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: 'app-contact-detail',
8 | template: `
9 |
10 |
11 |
{{contact.name.first}} {{contact.name.last}}
12 |
Company {{contact.company}}
13 |
14 |
15 |
16 |
17 |
Address
18 |
{{contact.address.street}}
19 | {{contact.address.city}}, {{contact.address.state}} {{contact.address.zip}}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | `,
29 | styles: [],
30 | standalone: false
31 | })
32 | export class ContactDetailComponent {
33 | @Input() contact;
34 | constructor() { }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/contacts/contact-list.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: 'app-contact-list',
11 | template: `
12 |
33 | `,
34 | styles: [],
35 | standalone: false
36 | })
37 | export class ContactListComponent {
38 | @Input() contacts;
39 | constructor() { }
40 | }
41 |
--------------------------------------------------------------------------------
/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: 'app-contact',
11 | template: `
12 |
27 | `,
28 | styles: [],
29 | standalone: false
30 | })
31 | export class ContactComponent {
32 | @Input() contact;
33 | constructor() { }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/contacts/contacts-data.service.ts:
--------------------------------------------------------------------------------
1 | import { AppConfigService } from '../global/app-config.service';
2 | import { Injectable } from '@angular/core';
3 | import { SessionStorage } from '../util/sessionStorage';
4 |
5 | export interface Contact {
6 | tags: any[];
7 | address: {
8 | zip: number;
9 | state: string;
10 | city: string;
11 | street: string;
12 | };
13 | phone: string;
14 | email: string;
15 | company: string;
16 | age: number;
17 | picture: string;
18 | _id: string;
19 | name: {
20 | last: string;
21 | first: string
22 | };
23 | }
24 |
25 | /** A fake Contacts REST client API */
26 | @Injectable()
27 | export class ContactsDataService extends SessionStorage {
28 | constructor(appConfig: AppConfigService) {
29 | // http://beta.json-generator.com/api/json/get/V1g6UwwGx
30 | super('contacts', 'assets/contacts.json', appConfig);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/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: 'app-contacts',
11 | template: `
12 |
21 | `,
22 | styles: [],
23 | standalone: false
24 | })
25 | export class ContactsComponent {
26 | @Input() contacts;
27 | constructor() { }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/contacts/contacts.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { ContactComponent } from './contact.component';
4 | import { ContactDetailComponent } from './contact-detail.component';
5 | import { ContactListComponent } from './contact-list.component';
6 | import { ContactsComponent } from './contacts.component';
7 | import { EditContactComponent } from './edit-contact.component';
8 | import { CONTACTS_STATES } from './contacts.states';
9 | import { UIRouterModule } from '@uirouter/angular';
10 | import { FormsModule } from '@angular/forms';
11 | import { ContactsDataService } from './contacts-data.service';
12 |
13 | @NgModule({
14 | imports: [
15 | UIRouterModule.forChild({ states: CONTACTS_STATES }),
16 | FormsModule,
17 | CommonModule
18 | ],
19 | declarations: [
20 | ContactComponent,
21 | ContactDetailComponent,
22 | ContactListComponent,
23 | ContactsComponent,
24 | EditContactComponent
25 | ],
26 | providers: [
27 | ContactsDataService
28 | ],
29 | })
30 | export class ContactsModule {
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/contacts/contacts.states.ts:
--------------------------------------------------------------------------------
1 | import {Ng2StateDeclaration} from '@uirouter/angular';
2 |
3 | import {ContactComponent} from './contact.component';
4 | import {ContactsComponent} from './contacts.component';
5 | import {EditContactComponent} from './edit-contact.component';
6 | import { ContactsDataService } from './contacts-data.service';
7 | import { Transition } from '@uirouter/core';
8 |
9 |
10 | export function getAllContacts(contactSvc) {
11 | return contactSvc.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 | component: ContactsComponent,
25 | resolve: [
26 | // Resolve all the contacts. The resolved contacts are injected into the controller.
27 | { token: 'contacts', deps: [ContactsDataService], resolveFn: getAllContacts },
28 | ],
29 | data: { requiresAuth: true },
30 | };
31 |
32 |
33 | export function getOneContact (contactSvc, $transition$) {
34 | return contactSvc.get($transition$.params().contactId);
35 | }
36 |
37 | /**
38 | * This state displays a single contact.
39 | * The contact to display is fetched using a resolve, based on the `contactId` parameter.
40 | */
41 | export const viewContactState: Ng2StateDeclaration = {
42 | name: 'contacts.contact',
43 | url: '/:contactId',
44 | component: ContactComponent,
45 | resolve: [
46 | // Resolve the contact, based on the contactId parameter value.
47 | // The resolved contact is provided to the contactComponent's contact binding
48 | { token: 'contact', deps: [ContactsDataService, Transition], resolveFn: getOneContact },
49 | ],
50 | };
51 |
52 |
53 | /**
54 | * This state allows a user to edit a contact
55 | *
56 | * The contact data to edit is injected from the parent state's resolve.
57 | *
58 | * This state uses view targeting to replace the parent ui-view (which would normally be filled
59 | * by 'contacts.contact') with the edit contact template/controller
60 | */
61 | export const editContactState: Ng2StateDeclaration = {
62 | name: 'contacts.contact.edit',
63 | url: '/edit',
64 | views: {
65 | // Relatively target the grand-parent-state's $default (unnamed) ui-view
66 | // This could also have been written using ui-view@state addressing: $default@contacts
67 | // Or, this could also have been written using absolute ui-view addressing: !$default.$default.$default
68 | '^.^.$default': {
69 | component: EditContactComponent,
70 | bindings: { pristineContact: 'contact' },
71 | }
72 | },
73 | };
74 |
75 |
76 | export function getBlankContact() {
77 | return { name: {}, address: {} };
78 | }
79 |
80 | /**
81 | * This state allows a user to create a new contact
82 | *
83 | * The contact data to edit is injected into the component from the parent state's resolve.
84 | */
85 | export const newContactState: Ng2StateDeclaration = {
86 | name: 'contacts.new',
87 | url: '/new',
88 | component: EditContactComponent,
89 | resolve: [
90 | { token: 'pristineContact', deps: [], resolveFn: getBlankContact }
91 | ],
92 | };
93 |
94 | export const CONTACTS_STATES = [
95 | contactsState,
96 | viewContactState,
97 | editContactState,
98 | newContactState,
99 | ];
100 |
--------------------------------------------------------------------------------
/src/app/contacts/edit-contact.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, Inject, OnDestroy } from '@angular/core';
2 | import { StateService, TransitionService, equals, StateDeclaration } from '@uirouter/core';
3 | import { DialogService } from '../global/dialog.service';
4 | import { ContactsDataService } from './contacts-data.service';
5 | import { copy } from '../util/util';
6 |
7 | /**
8 | * The EditContact component
9 | *
10 | * This component is used by both `contacts.contact.edit` and `contacts.new` states.
11 | *
12 | * The component makes a copy of the contqct data for editing.
13 | * The new copy and original (pristine) copy are used to determine if the contact is "dirty" or not.
14 | * If the user navigates to some other state while the contact is "dirty", the `uiCanExit` component
15 | * hook asks the user to confirm navigation away, losing any edits.
16 | *
17 | * The Delete Contact button is wired to the `remove` method, which:
18 | * - asks for confirmation from the user
19 | * - deletes the resource from REST API
20 | * - navigates back to the contacts grandparent state using relative addressing `^.^`
21 | * the `reload: true` option re-fetches the contacts list from the server
22 | *
23 | * The Save Contact button is wired to the `save` method which:
24 | * - saves the REST resource (PUT or POST, depending)
25 | * - navigates back to the parent state using relative addressing `^`.
26 | * when editing an existing contact, this returns to the `contacts.contact` "view contact" state
27 | * when creating a new contact, this returns to the `contacts` list.
28 | * the `reload: true` option re-fetches the contacts resolve data from the server
29 | */
30 | @Component({
31 | selector: 'app-edit-contact',
32 | template: `
33 |
57 | `,
58 | styles: [],
59 | standalone: false
60 | })
61 | export class EditContactComponent implements OnInit {
62 | @Input() pristineContact;
63 | contact;
64 | canExit: boolean;
65 |
66 | constructor(public $state: StateService,
67 | public dialogService: DialogService,
68 | public contactsService: ContactsDataService,
69 | // The state that is routing to the component, which could
70 | // be either contacts.new or contacts.contact.edit
71 | @Inject('$state$') public $state$: StateDeclaration,
72 | public transitionService: TransitionService) {
73 | }
74 |
75 | ngOnInit() {
76 | // Make an editable copy of the pristineContact
77 | this.contact = copy(this.pristineContact);
78 | }
79 |
80 | uiCanExit() {
81 | if (this.canExit || equals(this.contact, this.pristineContact)) {
82 | return true;
83 | }
84 |
85 | const message = 'You have unsaved changes to this contact.';
86 | const question = 'Navigate away and lose changes?';
87 | return this.dialogService.confirm(message, question);
88 | }
89 |
90 | /** Ask for confirmation, then delete the contact, then go to the grandparent state ('contacts') */
91 | remove(contact) {
92 | this.dialogService.confirm(`Delete contact: ${contact.name.first} ${contact.name.last}`)
93 | .then(() => this.contactsService.remove(contact))
94 | .then(() => this.canExit = true)
95 | .then(() => this.$state.go('^.^', null, { reload: true }));
96 | }
97 |
98 | /** Save the contact, then go to the parent state (either 'contacts' or 'contacts.contact') */
99 | save(contact) {
100 | this.contactsService.save(contact)
101 | .then(() => this.canExit = true)
102 | .then(() => this.$state.go('^', null, { reload: true }));
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/app/global/README.md:
--------------------------------------------------------------------------------
1 | ### The NgModule
2 | - *global.module*.ts: An Angular Module for global app code that doesn't make sense in a feature module
3 |
4 | ## NgModule Contents
5 |
6 | ### App Services
7 |
8 | - *app-config*.service.ts: Stores and retrieves the user's application preferences
9 | - *auth.service*.ts: Simulates an authentication service
10 | - *dialog.service*.ts: Provides a dialog confirmation (are you sure? yes/no) service
11 |
12 | ### Components
13 |
14 | - *dialog.component*.ts: A dialog component used by the dialog service
15 |
16 | ### Router Hooks
17 |
18 | - *auth.hook*.ts: A transition hook which allows a state to declare that it requires an authenticated user
19 |
--------------------------------------------------------------------------------
/src/app/global/app-config.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { BehaviorSubject } from 'rxjs';
3 |
4 | export interface SortOrder {
5 | sortBy: string;
6 | order: number;
7 | }
8 |
9 | /**
10 | * This service stores and retrieves user preferences in session storage
11 | */
12 | @Injectable()
13 | export class AppConfigService {
14 | private _sort = '+date';
15 | sort$ = new BehaviorSubject(this.parseSort(this.sort));
16 |
17 | get sort() { return this._sort; }
18 | set sort(val: string) {
19 | this._sort = val;
20 | this.sort$.next(this.parseSort(val));
21 | }
22 |
23 | emailAddress: string = undefined;
24 | restDelay = 100;
25 |
26 | constructor() {
27 | this.load();
28 | }
29 |
30 | toObject() {
31 | return {
32 | sort: this.sort,
33 | emailAddress: this.emailAddress,
34 | restDelay: this.restDelay,
35 | };
36 | }
37 |
38 | parseSort(sort: string): SortOrder {
39 | const defaultSort = '+date';
40 | const sortOrder = sort || defaultSort;
41 | const pattern = /^([+-])(.*)$/;
42 | const match = pattern.exec(sortOrder) || pattern.exec(defaultSort);
43 | const [__, order, sortBy] = match;
44 |
45 | return { sortBy, order: (order === '-' ? -1 : 1) };
46 | }
47 |
48 | load() {
49 | try {
50 | const data = JSON.parse(sessionStorage.getItem('appConfig'));
51 | return Object.assign(this, data);
52 | } catch (Error) { }
53 |
54 | return this;
55 | }
56 |
57 | save() {
58 | const string = JSON.stringify(this.toObject());
59 | sessionStorage.setItem('appConfig', string);
60 | }
61 |
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/src/app/global/auth.hook.ts:
--------------------------------------------------------------------------------
1 | import { TransitionService } from '@uirouter/core';
2 | import { AuthService } from './auth.service';
3 |
4 | /**
5 | * This file contains a Transition Hook which protects a
6 | * route that requires authentication.
7 | *
8 | * This hook redirects to /login when both:
9 | * - The user is not authenticated
10 | * - The user is navigating to a state that requires authentication
11 | */
12 | export function requiresAuthHook(transitionService: TransitionService) {
13 | // Matches if the destination state's data property has a truthy 'requiresAuth' property
14 | const 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 | const redirectToLogin = (transition) => {
22 | const authService: AuthService = transition.injector().get(AuthService);
23 | const $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 | transitionService.onBefore(requiresAuthCriteria, redirectToLogin, {priority: 10});
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/global/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { AppConfigService } from './app-config.service';
3 | import { wait } from '../util/util';
4 |
5 | /**
6 | * This service emulates an Authentication Service.
7 | */
8 | @Injectable()
9 | export class AuthService {
10 | // data
11 | usernames: string[] = ['myself@angular.dev', 'devgal@angular.dev', 'devguy@angular.dev'];
12 |
13 | constructor(public appConfig: AppConfigService) { }
14 |
15 | /**
16 | * Returns true if the user is currently authenticated, else false
17 | */
18 | isAuthenticated() {
19 | return !!this.appConfig.emailAddress;
20 | }
21 |
22 | /**
23 | * Fake authentication function that returns a promise that is either resolved or rejected.
24 | *
25 | * Given a username and password, checks that the username matches one of the known
26 | * usernames (this.usernames), and that the password matches 'password'.
27 | *
28 | * Delays 800ms to simulate an async REST API delay.
29 | */
30 | authenticate(username, password) {
31 | const appConfig = this.appConfig;
32 |
33 | // checks if the username is one of the known usernames, and the password is 'password'
34 | const checkCredentials = () => new Promise((resolve, reject) => {
35 | const validUsername = this.usernames.indexOf(username) !== -1;
36 | const validPassword = password === 'password';
37 |
38 | return (validUsername && validPassword) ? resolve(username) : reject('Invalid username or password');
39 | });
40 |
41 | return wait(800)
42 | .then(checkCredentials)
43 | .then((authenticatedUser: string) => {
44 | appConfig.emailAddress = authenticatedUser;
45 | appConfig.save();
46 | });
47 | }
48 |
49 | /** Logs the current user out */
50 | logout() {
51 | this.appConfig.emailAddress = undefined;
52 | this.appConfig.save();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/global/dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, HostBinding } from '@angular/core';
2 | import { wait } from '../util/util';
3 |
4 | @Component({
5 | selector: 'app-dialog',
6 | template: `
7 |
8 |
9 |
10 |
{{message}}
11 |
{{details}}
12 |
13 |
14 | {{yesMsg}}
15 | {{noMsg}}
16 |
17 |
18 |
19 | `,
20 | styles: [],
21 | standalone: false
22 | })
23 | export class DialogComponent implements OnInit {
24 | @HostBinding('class.dialog') dialog = true;
25 | @HostBinding('class.active') visible: boolean;
26 |
27 | message: string;
28 | details: string;
29 | yesMsg: string;
30 | noMsg: string;
31 |
32 | promise: Promise;
33 |
34 | constructor() {
35 | this.promise = new Promise((resolve, reject) => {
36 | this.yes = () => {
37 | this.visible = false;
38 | wait(300).then(resolve);
39 | };
40 |
41 | this.no = () => {
42 | this.visible = false;
43 | wait(300).then(reject);
44 | };
45 | });
46 | }
47 |
48 | yes() {}
49 | no() {}
50 |
51 | ngOnInit() {
52 | wait(50).then(() => this.visible = true);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/global/dialog.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, ViewContainerRef, ComponentFactoryResolver, Injector, ComponentFactory } from '@angular/core';
2 | import { DialogComponent } from './dialog.component';
3 |
4 | @Injectable()
5 | export class DialogService {
6 | vcRef: ViewContainerRef;
7 | private factory: ComponentFactory;
8 |
9 | constructor(resolver: ComponentFactoryResolver) {
10 | this.factory = resolver.resolveComponentFactory(DialogComponent);
11 | }
12 |
13 | confirm(message, details = 'Are you sure?', yesMsg = 'Yes', noMsg = 'No') {
14 | const componentRef = this.vcRef.createComponent(this.factory);
15 | const component = componentRef.instance;
16 |
17 | component.message = message;
18 | component.details = details;
19 | component.yesMsg = yesMsg;
20 | component.noMsg = noMsg;
21 |
22 | const destroy = () => componentRef.destroy();
23 | component.promise.then(destroy, destroy);
24 | return component.promise;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/global/global.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { AppConfigService } from './app-config.service';
4 | import { AuthService } from './auth.service';
5 | import { DialogComponent } from './dialog.component';
6 | import { DialogService } from './dialog.service';
7 |
8 | @NgModule({
9 | imports: [
10 | CommonModule
11 | ],
12 | providers: [
13 | AppConfigService,
14 | AuthService,
15 | DialogService,
16 | ],
17 | declarations: [DialogComponent]
18 | })
19 | export class GlobalModule { }
20 |
--------------------------------------------------------------------------------
/src/app/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | // This is a home component for authenticated users.
3 | // It shows giant buttons which activate their respective submodules: Messages, Contacts, Preferences
4 | @Component({
5 | selector: 'app-home',
6 | template: `
7 |
8 |
9 |
10 | Messages
11 |
12 |
13 |
14 |
15 | Contacts
16 |
17 |
18 |
19 |
20 | Preferences
21 |
22 |
23 | `,
24 | styles: [],
25 | standalone: false
26 | })
27 | export class HomeComponent {
28 | constructor() { }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/login.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input } from '@angular/core';
2 | import { TargetState, StateService } from '@uirouter/core';
3 | import { AuthService } from './global/auth.service';
4 | import { AppConfigService } from './global/app-config.service';
5 |
6 | /**
7 | * This component renders a faux authentication UI
8 | *
9 | * It prompts for the username/password (and gives hints with bouncy arrows)
10 | * It shows errors if the authentication failed for any reason.
11 | */
12 | @Component({
13 | selector: 'app-login',
14 | template: `
15 |
16 |
17 |
Log In
18 |
19 | (This login screen is for demonstration only...
20 | just pick a username, enter 'password' and click "Log in" )
21 |
22 |
23 |
24 | Username:
25 |
26 | {{username}}
27 |
28 |
29 | Choose
31 |
32 |
33 |
34 |
35 | Password:
36 |
37 |
39 | Enter 'password ' here
40 |
41 |
42 |
43 |
{{ errorMessage }}
44 |
45 |
46 |
47 |
48 | Log in
49 |
50 | Click Me!
52 |
53 |
54 |
55 | `,
56 | styles: [],
57 | standalone: false
58 | })
59 | export class LoginComponent {
60 | @Input() returnTo: TargetState;
61 |
62 | usernames: string[];
63 | credentials = { username: null, password: null };
64 | authenticating: boolean;
65 | errorMessage: string;
66 |
67 | constructor(appConfig: AppConfigService,
68 | private authService: AuthService,
69 | private $state: StateService
70 | ) {
71 | this.usernames = authService.usernames;
72 |
73 | this.credentials = {
74 | username: appConfig.emailAddress,
75 | password: 'password'
76 | };
77 | }
78 |
79 | /**
80 | * The controller for the `login` component
81 | *
82 | * The `login` method validates the credentials.
83 | * Then it sends the user back to the `returnTo` state, which is provided as a resolve data.
84 | */
85 | login(credentials) {
86 | this.authenticating = true;
87 |
88 | const returnToOriginalState = () => {
89 | const state = this.returnTo.state();
90 | const params = this.returnTo.params();
91 | const options = Object.assign({}, this.returnTo.options(), { reload: true });
92 | this.$state.go(state, params, options);
93 | };
94 |
95 | const showError = (errorMessage) =>
96 | this.errorMessage = errorMessage;
97 |
98 | const stop = () => this.authenticating = false;
99 | this.authService.authenticate(credentials.username, credentials.password)
100 | .then(returnToOriginalState)
101 | .catch(showError)
102 | .then(stop, stop);
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/src/app/main.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-main',
5 | template: `Loading... `,
6 | standalone: false
7 | })
8 | export class MainComponent {}
9 |
--------------------------------------------------------------------------------
/src/app/mymessages/compose.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, OnDestroy } from '@angular/core';
2 | import { Transition, StateService, equals, TransitionService } from '@uirouter/core';
3 | import { DialogService } from '../global/dialog.service';
4 | import { AppConfigService } from '../global/app-config.service';
5 | import { MessagesDataService } from './messages-data.service';
6 | import { copy } from '../util/util';
7 |
8 | /**
9 | * This component composes a message
10 | *
11 | * The message might be new, a saved draft, or a reply/forward.
12 | * A Cancel button discards the new message and returns to the previous state.
13 | * A Save As Draft button saves the message to the "drafts" folder.
14 | * A Send button sends the message
15 | */
16 | @Component({
17 | selector: 'app-compose',
18 | template: `
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Cancel
31 | Save as Draft
32 | Send
33 |
34 |
35 |
36 | `,
37 | styles: [],
38 | standalone: false
39 | })
40 | export class ComposeComponent implements OnInit {
41 | // data
42 | pristineMessage;
43 | message;
44 | canExit: boolean;
45 |
46 | constructor(public stateService: StateService,
47 | public transitionService: TransitionService,
48 | public DialogService: DialogService,
49 | public appConfig: AppConfigService,
50 | public messagesService: MessagesDataService,
51 | public transition: Transition,
52 | ) { }
53 |
54 | /**
55 | * Create our message's model using the current user's email address as 'message.from'
56 | * Then extend it with all the properties from (non-url) state parameter 'message'.
57 | * Keep two copies: the editable one and the original one.
58 | * These copies are used to check if the message is dirty.
59 | */
60 | ngOnInit() {
61 | const messageParam = this.transition.params().message;
62 | this.pristineMessage = Object.assign({from: this.appConfig.emailAddress}, messageParam);
63 | this.message = copy(this.pristineMessage);
64 | }
65 |
66 | /**
67 | * Checks if the edited copy and the pristine copy are identical when the state is changing.
68 | * If they are not identical, the allows the user to confirm navigating away without saving.
69 | */
70 | uiCanExit() {
71 | if (this.canExit || equals(this.pristineMessage, this.message)) {
72 | return true;
73 | }
74 |
75 | const message = 'You have not saved this message.';
76 | const question = 'Navigate away and lose changes?';
77 | return this.DialogService.confirm(message, question, 'Yes', 'No');
78 | }
79 |
80 | /**
81 | * Navigates back to the previous state.
82 | *
83 | * - Checks the $transition$ which activated this controller for a 'from state' that isn't the implicit root state.
84 | * - If there is no previous state (because the user deep-linked in, etc), then go to 'mymessages.messagelist'
85 | */
86 | gotoPreviousState() {
87 | const transition = this.transition;
88 | const hasPrevious = !!transition.from().name;
89 | const state = hasPrevious ? transition.from() : 'mymessages.messagelist';
90 | const params = hasPrevious ? transition.params('from') : {};
91 | this.stateService.go(state, params);
92 | };
93 |
94 | /** "Send" the message (save to the 'sent' folder), and then go to the previous state */
95 | send(message) {
96 | this.messagesService.save(Object.assign(message, {date: new Date(), read: true, folder: 'sent'}))
97 | .then(() => this.canExit = true)
98 | .then(() => this.gotoPreviousState());
99 | };
100 |
101 | /** Save the message to the 'drafts' folder, and then go to the previous state */
102 | save(message) {
103 | this.messagesService.save(Object.assign(message, {date: new Date(), read: true, folder: 'drafts'}))
104 | .then(() => this.canExit = true)
105 | .then(() => this.gotoPreviousState());
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/app/mymessages/folder-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | /**
4 | * Renders a list of folders
5 | */
6 | @Component({
7 | selector: 'app-folder-list',
8 | template: `
9 |
10 |
22 | `,
23 | styles: [],
24 | standalone: false
25 | })
26 | export class FolderListComponent {
27 | @Input() folders: any[];
28 | constructor() { }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/mymessages/folders-data.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { SessionStorage } from '../util/sessionStorage';
3 | import { AppConfigService } from '../global/app-config.service';
4 | import { Folder } from './interface';
5 |
6 | /** A fake REST client API for Folders resources */
7 | @Injectable()
8 | export class FoldersDataService extends SessionStorage {
9 | constructor(appConfig: AppConfigService) {
10 | super('folders', 'assets/folders.json', appConfig);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/mymessages/format-message.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'formatMessage',
5 | standalone: false
6 | })
7 | export class FormatMessagePipe implements PipeTransform {
8 | transform(value: string, args?: any): string {
9 | return value.split(/\n/).map(p => `${p}
`).join('\n');
10 | }
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/src/app/mymessages/interface.ts:
--------------------------------------------------------------------------------
1 | export interface Folder {
2 | _id: string;
3 | columns: string[];
4 | actions: string[];
5 | }
6 |
7 | export interface Message {
8 | read: boolean;
9 | folder: string;
10 | body: string;
11 | subject: string;
12 | from: string;
13 | to: string;
14 | date: string;
15 | senderName: {
16 | last: string;
17 | first: string
18 | };
19 | corpus: string;
20 | _id: string;
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/src/app/mymessages/message-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { Observable } from 'rxjs';
3 | import { Message } from './interface';
4 |
5 | /**
6 | * This component renders a list of messages using the `messageTable` component
7 | */
8 | @Component({
9 | selector: 'app-message-list',
10 | template: `
11 |
14 | `,
15 | styles: [],
16 | standalone: false
17 | })
18 | export class MessageListComponent {
19 | @Input() folder;
20 | @Input() messages$: Observable;
21 |
22 | constructor() { }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/mymessages/message-table.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 |
3 | /**
4 | * A component that displays a folder of messages as a table
5 | *
6 | * If a row is clicked, the details of the message is shown using a relative ui-sref to `.message`.
7 | *
8 | * ui-sref-active is used to highlight the selected row.
9 | *
10 | * Shows/hides specific columns based on the `columns` input binding.
11 | */
12 | @Component({
13 | selector: 'app-message-table',
14 | template: `
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
29 |
30 | {{ message.from }}
31 | {{ message.to }}
32 | {{ message.subject }}
33 | {{ message.date | date: "yyyy-MM-dd" }}
34 |
35 |
36 |
37 |
38 | `,
39 | styles: [],
40 | standalone: false
41 | })
42 | export class MessageTableComponent {
43 | @Input() columns: any[];
44 | @Input() messages: any[];
45 |
46 | constructor() { }
47 |
48 | colVisible(name) {
49 | return this.columns.indexOf(name) !== -1;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/mymessages/message.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input } from '@angular/core';
2 | import { setProp } from '../util/util';
3 | import { DialogService } from '../global/dialog.service';
4 | import { MessagesDataService } from './messages-data.service';
5 | import { StateService } from '@uirouter/core';
6 | import { Subscription , Observable } from 'rxjs';
7 | import { Folder, Message } from './interface';
8 |
9 | /**
10 | * This component renders a single message
11 | *
12 | * Buttons perform actions related to the message.
13 | * Buttons are shown/hidden based on the folder's context.
14 | * For instance, a "draft" message can be edited, but can't be replied to.
15 | */
16 | @Component({
17 | selector: 'app-message',
18 | template: `
19 |
20 |
21 |
48 |
49 |
50 |
51 |
52 | `,
53 | styles: [],
54 | standalone: false
55 | })
56 | export class MessageComponent implements OnInit {
57 | @Input() folder: Folder;
58 | @Input() message: Message;
59 |
60 | // What message should be activated if this message is deleted
61 | @Input() proximalMessage$: Observable;
62 | private proximalMessageSub: Subscription;
63 | proximalMessage: Message;
64 |
65 | // data
66 | actions;
67 |
68 | constructor(public stateService: StateService,
69 | public dialog: DialogService,
70 | public messagesService: MessagesDataService
71 | ) { }
72 |
73 | /**
74 | * When the user views a message, mark it as read and save (PUT) the resource.
75 | *
76 | * Apply the available actions for the message, depending on the folder the message belongs to.
77 | */
78 | ngOnInit() {
79 | this.message.read = true;
80 | this.messagesService.put(this.message);
81 |
82 | this.actions = this.folder.actions.reduce((obj, action) => setProp(obj, action, true), {});
83 | this.proximalMessageSub = this.proximalMessage$.subscribe(message => this.proximalMessage = message);
84 | }
85 |
86 | /**
87 | * Compose a new message as a reply to this one
88 | */
89 | reply(message) {
90 | const replyMsg = makeResponseMsg('Re: ', message);
91 | this.stateService.go('mymessages.compose', { message: replyMsg });
92 | };
93 |
94 | /**
95 | * Compose a new message as a forward of this one.
96 | */
97 | forward(message) {
98 | const fwdMsg = makeResponseMsg('Fwd: ', message);
99 | delete fwdMsg.to;
100 | this.stateService.go('mymessages.compose', { message: fwdMsg });
101 | };
102 |
103 | /**
104 | * Continue composing this (draft) message
105 | */
106 | editDraft(message) {
107 | this.stateService.go('mymessages.compose', { message: message });
108 | };
109 |
110 | /**
111 | * Delete this message.
112 | *
113 | * - confirm deletion
114 | * - delete the message
115 | * - determine which message should be active
116 | * - show that message
117 | */
118 | remove(message) {
119 | const nextMessageId = this.proximalMessage && this.proximalMessage._id;
120 | const nextState = nextMessageId ? 'mymessages.messagelist.message' : 'mymessages.messagelist';
121 | const params = { messageId: nextMessageId };
122 |
123 | this.dialog.confirm('Delete?', undefined)
124 | .then(() => this.messagesService.remove(message))
125 | .then(() => this.stateService.go(nextState, params, { reload: 'mymessages.messagelist' }));
126 | };
127 | }
128 |
129 |
130 |
131 | /** Helper function to prefix a message with "fwd: " or "re: " */
132 | const prefixSubject = (prefix, message) => prefix + message.subject;
133 | /** Helper function which quotes an email message */
134 | const quoteMessage = (message) => `
135 |
136 |
137 |
138 | ---------------------------------------
139 | Original message:
140 | From: ${message.from}
141 | Date: ${message.date}
142 | Subject: ${message.subject}
143 |
144 | ${message.body}`;
145 |
146 | /** Helper function to make a response message object */
147 | function makeResponseMsg(subjectPrefix, origMsg) {
148 | return {
149 | from: origMsg.to,
150 | to: origMsg.from,
151 | subject: prefixSubject(subjectPrefix, origMsg),
152 | body: quoteMessage(origMsg)
153 | };
154 | }
155 |
156 |
--------------------------------------------------------------------------------
/src/app/mymessages/messages-data.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { SessionStorage } from '../util/sessionStorage';
3 | import { AppConfigService, SortOrder } from '../global/app-config.service';
4 | import { Folder, Message } from './interface';
5 |
6 | /** A fake REST client API for Messages resources */
7 | @Injectable()
8 | export class MessagesDataService extends SessionStorage {
9 | static sortedMessages(messages: Message[], sortOrder: SortOrder): Message[] {
10 | const getField = (message: Message) =>
11 | message[sortOrder.sortBy].toString();
12 |
13 | return messages.slice().sort((a, b) =>
14 | getField(a).localeCompare(getField(b)) * sortOrder.order
15 | );
16 | }
17 |
18 | constructor(appConfig: AppConfigService) {
19 | // http://beta.json-generator.com/api/json/get/VJl5GbIze
20 | super('messages', 'assets/messages.json', appConfig);
21 | }
22 |
23 | byFolder(folder: Folder) {
24 | const searchObject = { folder: folder._id };
25 | const toFromAttr = ['drafts', 'sent'].indexOf(folder._id) !== -1 ? 'from' : 'to';
26 | searchObject[toFromAttr] = this.appConfig.emailAddress;
27 | return this.search(searchObject);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/mymessages/mymessages.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
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 | @Component({
11 | selector: 'app-mymessages',
12 | template: `
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | `,
26 | styles: [],
27 | standalone: false
28 | })
29 | export class MymessagesComponent {
30 | @Input() folders: any[];
31 |
32 | constructor() { }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/mymessages/mymessages.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { ComposeComponent } from './compose.component';
4 | import { MessageComponent } from './message.component';
5 | import { MessageListComponent } from './message-list.component';
6 | import { MymessagesComponent } from './mymessages.component';
7 | import { UIRouterModule } from '@uirouter/angular';
8 | import { MYMESSAGES_STATES } from './mymessages.states';
9 | import { FormsModule } from '@angular/forms';
10 | import { MessagesDataService } from './messages-data.service';
11 | import { FoldersDataService } from './folders-data.service';
12 | import { FolderListComponent } from './folder-list.component';
13 | import { MessageTableComponent } from './message-table.component';
14 | import { SortMessagesComponent } from './sort-messages.component';
15 | import { FormatMessagePipe } from './format-message.pipe';
16 |
17 | @NgModule({
18 | imports: [
19 | UIRouterModule.forChild({ states: MYMESSAGES_STATES }),
20 | FormsModule,
21 | CommonModule
22 | ],
23 | declarations: [
24 | ComposeComponent,
25 | MessageComponent,
26 | MessageListComponent,
27 | MymessagesComponent,
28 | FolderListComponent,
29 | MessageTableComponent,
30 | SortMessagesComponent,
31 | FormatMessagePipe,
32 | ],
33 | providers: [
34 | MessagesDataService,
35 | FoldersDataService,
36 | ]
37 | })
38 | export class MymessagesModule { }
39 |
--------------------------------------------------------------------------------
/src/app/mymessages/mymessages.states.ts:
--------------------------------------------------------------------------------
1 | import { Ng2StateDeclaration } from '@uirouter/angular';
2 | import { Transition } from '@uirouter/core';
3 | import { Observable } from 'rxjs';
4 | import { map } from 'rxjs/operators';
5 | import { AppConfigService } from '../global/app-config.service';
6 | import { ComposeComponent } from './compose.component';
7 | import { FoldersDataService } from './folders-data.service';
8 | import { Folder, Message } from './interface';
9 | import { MessageListComponent } from './message-list.component';
10 | import { MessageComponent } from './message.component';
11 | import { MessagesDataService } from './messages-data.service';
12 | import { MymessagesComponent } from './mymessages.component';
13 |
14 | export function getFolders(foldersService: FoldersDataService) {
15 | return foldersService.all();
16 | }
17 |
18 | /**
19 | * The mymessages state. This is the main state for the mymessages submodule.
20 | *
21 | * This state shows the list of folders for the current user. It retrieves the folders from the
22 | * Folders service. If a user navigates directly to this state, the state redirects to the 'mymessages.messagelist'.
23 | */
24 | export const mymessagesState: Ng2StateDeclaration = {
25 | parent: 'app',
26 | name: 'mymessages',
27 | url: '/mymessages',
28 | component: MymessagesComponent,
29 | resolve: [
30 | // All the folders are fetched from the Folders service
31 | { token: 'folders', deps: [FoldersDataService], resolveFn: getFolders },
32 | ],
33 | // If mymessages state is directly activated, redirect the transition to the child state 'mymessages.messagelist'
34 | redirectTo: 'mymessages.messagelist',
35 | // Mark this state as requiring authentication. See ../routerhooks/requiresAuth.js.
36 | data: { requiresAuth: true }
37 | };
38 |
39 |
40 | export function getFolder(foldersService: FoldersDataService, transition: Transition) {
41 | return foldersService.get(transition.params().folderId);
42 | }
43 |
44 | export function getMessages(messagesService: MessagesDataService, folder: Folder,
45 | appConfig: AppConfigService): Promise> {
46 | const promise = messagesService.byFolder(folder);
47 |
48 | return promise.then(messages => appConfig.sort$.pipe(map(sortOrder =>
49 | MessagesDataService.sortedMessages(messages, sortOrder)
50 | )));
51 | }
52 |
53 | /**
54 | * This state shows the contents (a message list) of a single folder
55 | */
56 | export const messageListState = {
57 | name: 'mymessages.messagelist',
58 | url: '/:folderId',
59 | // The folderId parameter is part of the URL. This params block sets 'inbox' as the default value.
60 | // If no parameter value for folderId is provided on the transition, then it will be defaulted to 'inbox'
61 | params: { folderId: 'inbox' },
62 | views: {
63 | // This targets the "messagelist" named ui-view added to the DOM in the parent state 'mymessages'
64 | messagelist: {
65 | component: MessageListComponent,
66 | },
67 | },
68 | resolve: [
69 | // Fetch the current folder from the Folders service, using the folderId parameter
70 | { token: 'folder', deps: [FoldersDataService, Transition], resolveFn: getFolder },
71 |
72 | // The folder object (from the resolve above) is injected into this resolve
73 | // The list of message for the folder are fetched from the Messages service
74 | { token: 'messages$', deps: [MessagesDataService, 'folder', AppConfigService], resolveFn: getMessages },
75 | ],
76 | };
77 |
78 |
79 | export function getMessage(messagesService: MessagesDataService, transition: Transition) {
80 | return messagesService.get(transition.params().messageId);
81 | }
82 |
83 | export function getProximalMessage(messages$: Observable, message: Message) {
84 | return messages$.pipe(
85 | map((messages: Message[]) => {
86 | const curIdx = messages.indexOf(message);
87 | const nextIdx = curIdx === messages.length ? curIdx - 1 : curIdx + 1;
88 | return messages[nextIdx];
89 | })
90 | );
91 | }
92 |
93 | /**
94 | * This state shows the contents of a single message.
95 | * It also has UI to reply, forward, delete, or edit an existing draft.
96 | */
97 | export const messageState: Ng2StateDeclaration = {
98 | name: 'mymessages.messagelist.message',
99 | url: '/:messageId',
100 | views: {
101 | // Relatively target the parent-state's parent-state's 'messagecontent' ui-view
102 | // This could also have been written using ui-view@state addressing: 'messagecontent@mymessages'
103 | // Or, this could also have been written using absolute ui-view addressing: '!$default.$default.messagecontent'
104 | '^.^.messagecontent': {
105 | component: MessageComponent,
106 | },
107 | },
108 | resolve: [
109 | // Fetch the message from the Messages service using the messageId parameter
110 | { token: 'message', deps: [MessagesDataService, Transition], resolveFn: getMessage },
111 |
112 | // Provide the component with the next closest message to activate if the current message is deleted
113 | { token: 'proximalMessage$', deps: ['messages$', 'message'], resolveFn: getProximalMessage }
114 | ],
115 | };
116 |
117 |
118 | /**
119 | * This state allows the user to compose a new message, edit a drafted message, send a message,
120 | * or save an unsent message as a draft.
121 | *
122 | * This state uses view-targeting to take over the ui-view that would normally be filled by the 'mymessages' state.
123 | */
124 | export const composeState: Ng2StateDeclaration = {
125 | name: 'mymessages.compose',
126 | url: '/compose',
127 | views: {
128 | // Absolutely targets the $default (unnamed) ui-view, two nesting levels down with the composeComponent.
129 | '!$default.$default': { component: ComposeComponent }
130 | },
131 | // Declares that this state has a 'message' parameter, that defaults to an empty object.
132 | // Note the parameter does not appear in the URL.
133 | params: {
134 | message: {}
135 | },
136 | };
137 |
138 | export const MYMESSAGES_STATES = [
139 | mymessagesState,
140 | messageListState,
141 | messageState,
142 | composeState,
143 | ];
144 |
--------------------------------------------------------------------------------
/src/app/mymessages/sort-messages.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, HostListener, OnDestroy } from '@angular/core';
2 | import { AppConfigService } from '../global/app-config.service';
3 | import { Subscription } from 'rxjs';
4 |
5 | /**
6 | * A directive (for a table header) which changes the app's sort order
7 | */
8 | @Component({
9 | selector: '[app-sort-messages]',
10 | template: `
11 | {{ label }}
16 | `,
17 | styles: [],
18 | standalone: false
19 | })
20 | export class SortMessagesComponent implements OnInit, OnDestroy {
21 | @Input('prop') prop: string;
22 | @Input('label') label: string;
23 | private _sub: Subscription;
24 | public asc: boolean;
25 | public desc: boolean;
26 |
27 | constructor(private appConfig: AppConfigService) { }
28 |
29 | ngOnInit() {
30 | this._sub = this.appConfig.sort$.subscribe(() => this.update());
31 | }
32 |
33 | ngOnDestroy() {
34 | this._sub.unsubscribe();
35 | }
36 |
37 | update() {
38 | const sort = this.appConfig.sort$.value;
39 | const matches = sort.sortBy === this.prop;
40 | this.asc = matches && sort.order < 0;
41 | this.desc = matches && sort.order > 0;
42 | }
43 |
44 | @HostListener('click', ['$event'])
45 | onClick(e) {
46 | this.appConfig.sort = (this.asc ? '+' : '-') + this.prop;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/prefs/README.md:
--------------------------------------------------------------------------------
1 | ### The NgModule
2 |
3 | - *prefs.module*.ts: A feature module (NgModule) which manages user preferences
4 |
5 | ## NgModule Contents
6 |
7 | ### The prefs states
8 |
9 | - *prefs.states*.ts: Defines the ui-router states for preferences feature
10 |
11 | ### The prefs components
12 |
13 | - *prefs.component*.js: A component for showing and/or updating user preferences.
14 |
--------------------------------------------------------------------------------
/src/app/prefs/prefs.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { AppConfigService } from '../global/app-config.service';
3 |
4 | /**
5 | * A component which shows and updates app preferences
6 | */
7 | @Component({
8 | selector: 'app-prefs',
9 | template: `
10 |
11 | Reset All Data
12 |
13 |
14 |
15 | Simulated REST API delay (ms)
16 |
17 | Save
18 |
19 | `,
20 | styles: [],
21 | standalone: false
22 | })
23 | export class PrefsComponent {
24 | // data
25 | prefs;
26 |
27 | constructor(private appConfig: AppConfigService) {
28 | this.prefs = {
29 | restDelay: appConfig.restDelay
30 | };
31 | }
32 |
33 | /** Clear out the session storage */
34 | reset() {
35 | sessionStorage.clear();
36 | document.location.reload();
37 | }
38 |
39 | /** After saving preferences to session storage, reload the entire application */
40 | savePrefs() {
41 | Object.assign(this.appConfig, { restDelay: this.prefs.restDelay }).save();
42 | document.location.reload();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/prefs/prefs.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { PrefsComponent } from './prefs.component';
4 | import { prefsState } from './prefs.states';
5 | import { UIRouterModule } from '@uirouter/angular';
6 | import { FormsModule } from '@angular/forms';
7 |
8 | @NgModule({
9 | imports: [
10 | UIRouterModule.forChild({ states: [ prefsState ] }),
11 | FormsModule,
12 | CommonModule
13 | ],
14 | declarations: [
15 | PrefsComponent
16 | ]
17 | })
18 | export class PrefsModule { }
19 |
--------------------------------------------------------------------------------
/src/app/prefs/prefs.states.ts:
--------------------------------------------------------------------------------
1 | import { PrefsComponent } from './prefs.component';
2 | /**
3 | * This state allows the user to set their application preferences
4 | */
5 | export const prefsState = {
6 | parent: 'app',
7 | name: 'prefs',
8 | url: '/prefs',
9 | component: PrefsComponent,
10 | // Mark this state as requiring authentication. See ../global/auth.hook
11 | data: { requiresAuth: true }
12 | };
13 |
--------------------------------------------------------------------------------
/src/app/router.config.ts:
--------------------------------------------------------------------------------
1 | import { UIRouter, Category } from '@uirouter/core';
2 | import { Visualizer } from '@uirouter/visualizer';
3 |
4 | import { googleAnalyticsHook } from './util/ga';
5 | import { requiresAuthHook } from './global/auth.hook';
6 |
7 | export function routerConfigFn(router: UIRouter) {
8 | const transitionService = router.transitionService;
9 | requiresAuthHook(transitionService);
10 | googleAnalyticsHook(transitionService);
11 |
12 | router.trace.enable(Category.TRANSITION);
13 | router.plugin(Visualizer);
14 | }
15 |
--------------------------------------------------------------------------------
/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 | /** Google analytics */
2 | /* tslint:disable */
3 |
4 | import { TransitionService } from '@uirouter/core';
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 | export function googleAnalyticsHook(transitionService: TransitionService) {
14 | const vpv = (vpath) =>
15 | window['ga']('send', 'pageview', vpath);
16 |
17 | const path = (trans) => {
18 | const formattedRoute = trans.$to().url.format(trans.params());
19 | const withSitePrefix = location.pathname + formattedRoute;
20 | return `/${withSitePrefix.split('/').filter(x => x).join('/')}`;
21 | };
22 |
23 | const error = (trans) => {
24 | const err = trans.error();
25 | const type = err && err.hasOwnProperty('type') ? err.type : '_';
26 | const message = err && err.hasOwnProperty('message') ? err.message : '_';
27 | vpv(path(trans) + ';errorType=' + type + ';errorMessage=' + message);
28 | };
29 |
30 | transitionService.onSuccess({}, (trans) => vpv(path(trans)), { priority: -10000 });
31 | transitionService.onError({}, (trans) => error(trans), { priority: -10000 });
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/util/sessionStorage.ts:
--------------------------------------------------------------------------------
1 | import { pushToArr, guid, wait } from './util';
2 | import { AppConfigService } from '../global/app-config.service';
3 |
4 | /**
5 | * This class simulates a RESTful resource, but the API calls fetch data from
6 | * Session Storage instead of an HTTP call.
7 | *
8 | * Once configured, it loads the initial (pristine) data from the URL provided (using HTTP).
9 | * It exposes GET/PUT/POST/DELETE-like API that operates on the data. All the data is also
10 | * stored in Session Storage. If any data is modified in memory, session storage is updated.
11 | * If the browser is refreshed, the SessionStorage object will try to fetch the existing data from
12 | * the session, before falling back to re-fetching the initial data using HTTP.
13 | *
14 | * For an example, please see dataSources.js
15 | */
16 | export class SessionStorage {
17 | // data
18 | _data: Promise;
19 | _idProp: string;
20 | _eqFn: (a: T, b: T) => boolean;
21 |
22 | /**
23 | * Creates a new SessionStorage object
24 | *
25 | * @param sessionStorageKey The session storage key. The data will be stored in browser's session storage under this key.
26 | * @param sourceUrl The url that contains the initial data.
27 | * @param appConfig Pass in the AppConfig object
28 | */
29 | constructor(public sessionStorageKey, sourceUrl, public appConfig: AppConfigService) {
30 | let data;
31 | const fromSession = sessionStorage.getItem(sessionStorageKey);
32 | // A promise for *all* of the data.
33 | this._data = undefined;
34 |
35 | // For each data object, the _idProp defines which property has that object's unique identifier
36 | this._idProp = '_id';
37 |
38 | // A basic triple-equals equality checker for two values
39 | this._eqFn = (l, r) => l[this._idProp] === r[this._idProp];
40 |
41 | if (fromSession) {
42 | try {
43 | // Try to parse the existing data from the Session Storage API
44 | data = JSON.parse(fromSession);
45 | } catch (e) {
46 | console.log('Unable to parse session messages, retrieving intial data.');
47 | }
48 | }
49 |
50 | // Create a promise for the data; Either the existing data from session storage, or the initial data via $http request
51 | this._data = (data ? Promise.resolve(data) : fetch(sourceUrl)
52 | .then(resp => resp.json()))
53 | .then(this._commit.bind(this))
54 | .then(() => JSON.parse(sessionStorage.getItem(sessionStorageKey)));
55 | }
56 |
57 | /** Saves all the data back to the session storage */
58 | _commit(data: T[]): Promise {
59 | sessionStorage.setItem(this.sessionStorageKey, JSON.stringify(data));
60 | return Promise.resolve(data);
61 | }
62 |
63 | /** Helper which simulates a delay, then provides the `thenFn` with the data */
64 | all(): Promise {
65 | const delay = this.appConfig.restDelay;
66 | return wait(delay).then(() => this._data);
67 | }
68 |
69 | /** Given a sample item, returns a promise for all the data for items which have the same properties as the sample */
70 | search(exampleItem): Promise {
71 | const contains = (search, inString) =>
72 | ('' + inString).indexOf('' + search) !== -1;
73 | const matchesExample = (example, item) =>
74 | Object.keys(example).reduce((memo, key) => memo && contains(example[key], item[key]), true);
75 | return this.all().then(items =>
76 | items.filter(matchesExample.bind(null, exampleItem)));
77 | }
78 |
79 | /** Returns a promise for the item with the given identifier */
80 | get(id): Promise {
81 | return this.all().then(items =>
82 | items.find(item => item[this._idProp] === id));
83 | }
84 |
85 | /** Returns a promise to save the item. It delegates to put() or post() if the object has or does not have an identifier set */
86 | save(item: T): Promise {
87 | return item[this._idProp] ? this.put(item) : this.post(item);
88 | }
89 |
90 | /** Returns a promise to save (POST) a new item. The item's identifier is auto-assigned. */
91 | post(item: T): Promise {
92 | item[this._idProp] = guid();
93 | return this.all()
94 | .then(items => pushToArr(items, item))
95 | .then(this._commit.bind(this))
96 | .then(() => item);
97 | }
98 |
99 | /** Returns a promise to save (PUT) an existing item. */
100 | put(item: T, eqFn = this._eqFn): Promise {
101 | return this.all().then(items => {
102 | const idx = items.findIndex(eqFn.bind(null, item));
103 | if (idx === -1) {
104 | throw Error(`${item} not found in ${this}`);
105 | }
106 | items[idx] = item;
107 | return this._commit(items).then(() => item);
108 | });
109 | }
110 |
111 | /** Returns a promise to remove (DELETE) an item. */
112 | remove(item: T, eqFn = this._eqFn): Promise {
113 | return this.all().then(items => {
114 | const idx = items.findIndex(eqFn.bind(null, item));
115 | if (idx === -1) {
116 | throw Error(`${item} not found in ${this}`);
117 | }
118 | items.splice(idx, 1);
119 | return this._commit(items).then(() => item);
120 | });
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/app/util/util.ts:
--------------------------------------------------------------------------------
1 | import { pattern, isObject, identity, val } from '@uirouter/core';
2 | /** Some utility functions used by the application */
3 |
4 | export const setProp = (obj, key, val) => { obj[key] = val; return obj; };
5 | export const pushToArr = (array, item) => { array.push(item); return array; };
6 | export const uniqReduce = (arr, item) => arr.indexOf(item) !== -1 ? arr : pushToArr(arr, item);
7 | export const flattenReduce = (arr, item) => arr.concat(item);
8 | const guidChar = (c) => c !== 'x' && c !== 'y' ? '-' : Math.floor(Math.random() * 16).toString(16).toUpperCase();
9 | export const guid = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.split('').map(guidChar).join('');
10 | // A function that returns a promise which resolves after a timeout
11 | export const wait = (delay) => new Promise(resolve => setTimeout(resolve, delay));
12 |
13 | export const copy = pattern([
14 | [Array.isArray, val => val.map(copy)],
15 | [isObject, val => Object.keys(val).reduce((acc, key) => (acc[key] = copy(val[key]), acc), {})],
16 | [val(true), identity ]
17 | ]);
18 |
19 |
--------------------------------------------------------------------------------
/src/app/welcome.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-welcome',
5 | template: `
6 |
7 |
8 |
UI-Router Sample App
9 |
10 |
Welcome to the sample app!
11 |
This is a demonstration app intended to highlight some patterns that can be used within UI-Router.
12 | These patterns should help you to to build cohesive, robust apps. Additionally, this app uses state-vis
13 | to show the tree of states, and a transition log visualizer.
14 |
15 |
App Overview
16 |
17 | First, start exploring the application's functionality at a high level by activating
18 | one of the three submodules: Messages, Contacts, or Preferences. If you are not already logged in,
19 | you will be taken to an authentication screen (the authentication is fake; the password is "password")
20 |
21 |
22 | Messages
23 | Contacts
24 | Preferences
25 |
26 |
27 |
Patterns and Recipes
28 |
29 | Require Authentication
30 | Previous State
31 | Redirect Hook
32 | Default Param Values
33 |
34 |
35 | `,
36 | styles: [],
37 | standalone: false
38 | })
39 | export class WelcomeComponent implements OnInit {
40 | constructor() { }
41 |
42 | ngOnInit() {
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/assets/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/assets/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/assets/corpora/2nd-treatise.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/2nd-treatise.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/beatles.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/beatles.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/beowulf.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/beowulf.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/bsdfaq.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/bsdfaq.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/cat-in-the-hat.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/cat-in-the-hat.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/comm_man.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/comm_man.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/elflore.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/elflore.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/flatland.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/flatland.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/green-eggs.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/green-eggs.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/macbeth.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/macbeth.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/palin.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/palin.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/rfc2549.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/rfc2549.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/rfc7230.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/rfc7230.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/sneetches.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/sneetches.txt.gz
--------------------------------------------------------------------------------
/src/assets/corpora/two-cities.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ui-router/sample-app-angular/3a7add8438549c80556cf5b420663b48bc5d59b7/src/assets/corpora/two-cities.txt.gz
--------------------------------------------------------------------------------
/src/assets/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/assets/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/assets/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/assets/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/3a7add8438549c80556cf5b420663b48bc5d59b7/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SampleAppNg2
6 |
7 |
8 |
9 |
10 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
2 | import { enableProdMode } from '@angular/core';
3 | import { environment } from './environments/environment';
4 | import { AppModule } from './app/app.module';
5 |
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule);
12 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js';
2 |
--------------------------------------------------------------------------------
/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/dist/long-stack-trace-zone';
4 | import 'zone.js/dist/proxy.js';
5 | import 'zone.js/dist/sync-test';
6 | import 'zone.js/dist/jasmine-patch';
7 | import 'zone.js/dist/async-test';
8 | import 'zone.js/dist/fake-async-test';
9 | import { getTestBed } from '@angular/core/testing';
10 | import {
11 | BrowserDynamicTestingModule,
12 | platformBrowserDynamicTesting
13 | } from '@angular/platform-browser-dynamic/testing';
14 |
15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
16 | declare var __karma__: any;
17 |
18 | // Prevent Karma from running prematurely.
19 | __karma__.loaded = function () {};
20 |
21 | // First, initialize the Angular testing environment.
22 | getTestBed().initTestEnvironment(
23 | BrowserDynamicTestingModule,
24 | platformBrowserDynamicTesting()
25 | );
26 | // Finally, start Karma to run the tests.
27 | __karma__.start();
28 |
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "",
4 | "declaration": false,
5 | "downlevelIteration": true,
6 | "experimentalDecorators": true,
7 | "lib": [
8 | "es6",
9 | "dom"
10 | ],
11 | "mapRoot": "./",
12 | "module": "es2020",
13 | "moduleResolution": "node",
14 | "outDir": "../dist/out-tsc",
15 | "sourceMap": true,
16 | "target": "ES2022",
17 | "skipLibCheck": true,
18 | "typeRoots": [
19 | "../node_modules/@types"
20 | ],
21 | "useDefineForClassFields": false
22 | },
23 | "files": [
24 | "main.ts",
25 | "polyfills.ts"
26 | ],
27 | "include": [
28 | "src/**/*.d.ts"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /*
2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience.
3 | It is not intended to be used to perform a compilation.
4 |
5 | To learn more about this file see: https://angular.io/config/solution-tsconfig.
6 | */
7 | {
8 | "compilerOptions": {
9 | "esModuleInterop": true
10 | },
11 | "files": [],
12 | "references": [
13 | {
14 | "path": "./src/tsconfig.json"
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "callable-types": true,
7 | "class-name": true,
8 | "comment-format": [
9 | true,
10 | "check-space"
11 | ],
12 | "curly": true,
13 | "eofline": true,
14 | "forin": true,
15 | "import-blacklist": [true],
16 | "import-spacing": true,
17 | "indent": [
18 | true,
19 | "spaces"
20 | ],
21 | "interface-over-type-literal": true,
22 | "label-position": true,
23 | "max-line-length": [
24 | true,
25 | 140
26 | ],
27 | "member-access": false,
28 | "member-ordering": [
29 | true,
30 | "static-before-instance",
31 | "variables-before-functions"
32 | ],
33 | "no-arg": true,
34 | "no-bitwise": true,
35 | "no-console": [
36 | true,
37 | "debug",
38 | "info",
39 | "time",
40 | "timeEnd",
41 | "trace"
42 | ],
43 | "no-construct": true,
44 | "no-debugger": true,
45 | "no-duplicate-variable": true,
46 | "no-empty": false,
47 | "no-empty-interface": true,
48 | "no-eval": true,
49 | "no-inferrable-types": true,
50 | "no-shadowed-variable": true,
51 | "no-string-literal": false,
52 | "no-string-throw": true,
53 | "no-switch-case-fall-through": true,
54 | "no-trailing-whitespace": true,
55 | "no-unused-expression": true,
56 | "no-use-before-declare": true,
57 | "no-var-keyword": true,
58 | "object-literal-sort-keys": false,
59 | "one-line": [
60 | true,
61 | "check-open-brace",
62 | "check-catch",
63 | "check-else",
64 | "check-whitespace"
65 | ],
66 | "prefer-const": true,
67 | "quotemark": [
68 | true,
69 | "single"
70 | ],
71 | "radix": true,
72 | "semicolon": [
73 | "always"
74 | ],
75 | "triple-equals": [
76 | true,
77 | "allow-null-check"
78 | ],
79 | "typedef-whitespace": [
80 | true,
81 | {
82 | "call-signature": "nospace",
83 | "index-signature": "nospace",
84 | "parameter": "nospace",
85 | "property-declaration": "nospace",
86 | "variable-declaration": "nospace"
87 | }
88 | ],
89 | "typeof-compare": true,
90 | "unified-signatures": true,
91 | "variable-name": false,
92 | "whitespace": [
93 | true,
94 | "check-branch",
95 | "check-decl",
96 | "check-operator",
97 | "check-separator",
98 | "check-type"
99 | ],
100 |
101 | "directive-selector": [true, "attribute", "app", "camelCase"],
102 | "component-selector": [true, "element", "app", "kebab-case"],
103 | "no-inputs-metadata-property": true,
104 | "no-outputs-metadata-property": true,
105 | "no-host-metadata-property": true,
106 | "no-input-rename": true,
107 | "no-output-rename": true,
108 | "use-lifecycle-interface": true,
109 | "use-pipe-transform-interface": true,
110 | "component-class-suffix": true,
111 | "directive-class-suffix": true,
112 | "no-access-missing-member": true,
113 | "templates-use-public": true,
114 | "invoke-injectable": true
115 | }
116 | }
117 |
--------------------------------------------------------------------------------