29 | `
30 | })
31 | export class ContactDetail {
32 | @Input() contact;
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/contacts/contactList.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from "@angular/core";
2 |
3 | /**
4 | * This component renders a list of contacts.
5 | *
6 | * At the top is a "new contact" button.
7 | * Each list item is a clickable link to the `contacts.contact` details substate
8 | */
9 | @Component({
10 | selector: 'contact-list',
11 | standalone: false,
12 | template: `
13 |
34 | `})
35 | export class ContactList {
36 | @Input() contacts;
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/contacts/contacts.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from "@angular/core";
2 |
3 | /**
4 | * This component renders the contacts submodule.
5 | *
6 | * On the left is the list of contacts.
7 | * On the right is the ui-view viewport where contact details appear.
8 | */
9 | @Component({
10 | selector: 'contacts',
11 | standalone: false,
12 | template: `
13 |
14 |
15 |
16 |
17 |
18 |
19 |
Select a contact
20 |
21 |
22 | `})
23 | export class Contacts {
24 | @Input() contacts;
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/contacts/contacts.module.ts:
--------------------------------------------------------------------------------
1 | import {FormsModule} from '@angular/forms';
2 | import {CommonModule} from "@angular/common";
3 | import {NgModule} from "@angular/core";
4 | import {UIRouterModule} from "@uirouter/angular";
5 |
6 | import {contactsState, editContactState, newContactState, viewContactState} from "./contacts.states";
7 | import {ContactDetail} from "./contactDetail.component";
8 | import {ContactList} from "./contactList.component";
9 | import {Contact} from "./contact.component";
10 | import {Contacts} from "./contacts.component";
11 | import {EditContact} from "./editContact.component";
12 |
13 | export let CONTACTS_STATES = [contactsState, newContactState, viewContactState, editContactState];
14 |
15 | /** The NgModule for Contacts feature */
16 | @NgModule({
17 | imports: [
18 | CommonModule,
19 | FormsModule,
20 | UIRouterModule.forChild({ states: CONTACTS_STATES })
21 | ],
22 | declarations: [Contact, ContactDetail, ContactList, Contacts, EditContact],
23 | })
24 | class ContactsModule {}
25 |
26 | export {ContactsModule};
27 |
--------------------------------------------------------------------------------
/src/app/contacts/contacts.states.ts:
--------------------------------------------------------------------------------
1 | import {Ng2StateDeclaration} from "@uirouter/angular";
2 |
3 | import {Contact} from "./contact.component";
4 | import {Contacts} from "./contacts.component";
5 | import {EditContact} from "./editContact.component";
6 |
7 |
8 | // Resolve all the contacts. The resolved contacts are injected into the controller.
9 | resolveContacts.$inject = ['Contacts'];
10 | export function resolveContacts(Contacts) {
11 | return Contacts.all();
12 | }
13 |
14 | /**
15 | * This state displays the contact list.
16 | * It also provides a nested ui-view (viewport) for child states to fill in.
17 | *
18 | * The contacts are fetched using a resolve.
19 | */
20 | export const contactsState: Ng2StateDeclaration = {
21 | parent: 'app', // declares that 'contacts' is a child of 'app'
22 | name: "contacts",
23 | url: "/contacts",
24 | resolve: {
25 | contacts: resolveContacts
26 | },
27 | data: { requiresAuth: true },
28 | component: Contacts
29 | };
30 |
31 | resolveContact.$inject = ['Contacts', '$transition$'];
32 | export function resolveContact(Contacts, $transition$) {
33 | return Contacts.get($transition$.params().contactId);
34 | }
35 |
36 | /**
37 | * This state displays a single contact.
38 | * The contact to display is fetched using a resolve, based on the `contactId` parameter.
39 | */
40 | export const viewContactState: Ng2StateDeclaration = {
41 | name: 'contacts.contact',
42 | url: '/:contactId',
43 | resolve: {
44 | // Resolve the contact, based on the contactId parameter value.
45 | // The resolved contact is provided to the contactComponent's contact binding
46 | contact: resolveContact
47 | },
48 | component: Contact
49 | };
50 |
51 |
52 | /**
53 | * This state allows a user to edit a contact
54 | *
55 | * The contact data to edit is injected from the parent state's resolve.
56 | *
57 | * This state uses view targeting to replace the parent ui-view (which would normally be filled
58 | * by 'contacts.contact') with the edit contact template/controller
59 | */
60 | export const editContactState: Ng2StateDeclaration = {
61 | name: 'contacts.contact.edit',
62 | url: '/edit',
63 | views: {
64 | // Relatively target the grand-parent-state's $default (unnamed) ui-view
65 | // This could also have been written using ui-view@state addressing: $default@contacts
66 | // Or, this could also have been written using absolute ui-view addressing: !$default.$default.$default
67 | '^.^.$default': {
68 | bindings: { pristineContact: "contact" },
69 | component: EditContact
70 | }
71 | }
72 | };
73 |
74 | export function resolvePristineContact() {
75 | return { name: {}, address: {} };
76 | }
77 | /**
78 | * This state allows a user to create a new contact
79 | *
80 | * The contact data to edit is injected into the component from the parent state's resolve.
81 | */
82 | export const newContactState: Ng2StateDeclaration = {
83 | name: 'contacts.new',
84 | url: '/new',
85 | resolve: {
86 | pristineContact: resolvePristineContact
87 | },
88 | component: EditContact
89 | };
90 |
--------------------------------------------------------------------------------
/src/app/contacts/editContact.component.ts:
--------------------------------------------------------------------------------
1 | import * as angular from "angular";
2 | import {UIView} from "@uirouter/angular";
3 | import {StateService, TransitionService} from "@uirouter/core";
4 | import {Component, Input, Inject, Optional} from "@angular/core";
5 |
6 | /**
7 | * The EditContact component
8 | *
9 | * This component is used by both `contacts.contact.edit` and `contacts.new` states.
10 | *
11 | * The component makes a copy of the contqct data for editing.
12 | * The new copy and original (pristine) copy are used to determine if the contact is "dirty" or not.
13 | * If the user navigates to some other state while the contact is "dirty", the `uiCanExit` component
14 | * hook asks the user to confirm navigation away, losing any edits.
15 | *
16 | * The Delete Contact button is wired to the `remove` method, which:
17 | * - asks for confirmation from the user
18 | * - deletes the resource from REST API
19 | * - navigates back to the contacts grandparent state using relative addressing `^.^`
20 | * the `reload: true` option re-fetches the contacts list from the server
21 | *
22 | * The Save Contact button is wired to the `save` method which:
23 | * - saves the REST resource (PUT or POST, depending)
24 | * - navigates back to the parent state using relative addressing `^`.
25 | * when editing an existing contact, this returns to the `contacts.contact` "view contact" state
26 | * when creating a new contact, this returns to the `contacts` list.
27 | * the `reload: true` option re-fetches the contacts resolve data from the server
28 | */
29 | @Component({
30 | selector: 'edit-contact',
31 | standalone: false,
32 | template: `
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | `})
58 | export class EditContact {
59 | @Input() pristineContact;
60 | contact;
61 | state;
62 | deregister;
63 | canExit: boolean;
64 |
65 | // Note: you can inject StateService and TransitionService from @uirouter/core
66 | constructor(public $state: StateService,
67 | @Inject('DialogService') public DialogService,
68 | @Inject('Contacts') public Contacts,
69 | @Optional() @Inject(UIView.PARENT_INJECT) view,
70 | public $trans: TransitionService) {
71 | this.state = view && view.context && view.context.name;
72 | }
73 |
74 | ngOnInit() {
75 | // Make an editable copy of the pristineContact
76 | this.contact = angular.copy(this.pristineContact);
77 | this.deregister = this.$trans.onBefore({ exiting: this.state }, () => this.uiCanExit());
78 | }
79 |
80 | ngOnDestroy() {
81 | if (this.deregister) this.deregister();
82 | }
83 |
84 | uiCanExit() {
85 | if (this.canExit || angular.equals(this.contact, this.pristineContact)) {
86 | return true;
87 | }
88 |
89 | let message = 'You have unsaved changes to this contact.';
90 | let question = 'Navigate away and lose changes?';
91 | return this.DialogService.confirm(message, question);
92 | }
93 |
94 | /** Ask for confirmation, then delete the contact, then go to the grandparent state ('contacts') */
95 | remove(contact) {
96 | this.DialogService.confirm(`Delete contact: ${contact.name.first} ${contact.name.last}`)
97 | .then(() => this.Contacts.remove(contact))
98 | .then(() => this.canExit = true)
99 | .then(() => this.$state.go("^.^", null, { reload: true }));
100 | }
101 |
102 | /** Save the contact, then go to the parent state (either 'contacts' or 'contacts.contact') */
103 | save(contact) {
104 | this.Contacts.save(contact)
105 | .then(() => this.canExit = true)
106 | .then(() => this.$state.go("^", null, { reload: true }));
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/app/global/README.md:
--------------------------------------------------------------------------------
1 | ## Contents
2 |
3 | ### Global services
4 | - *appConfig*.service.js: Stores and retrieves the user's application preferences
5 | - *auth*.service.js: Simulates an authentication service
6 | - *dataSources*.service.js: Provides REST-like client API for Folders, Messages, and Contacts
7 | - *dialog*.service.js: Provides a dialog confirmation service
8 |
9 | ### Directives
10 | - *dialog*.directive.js: Provides a dialog directive used by the dialog service
11 |
12 | ### Router Hooks
13 |
14 | - *requiresAuth*.hook.js: A transition hook which allows a state to declare that it requires an authenticated user
--------------------------------------------------------------------------------
/src/app/global/appConfig.service.ts:
--------------------------------------------------------------------------------
1 | import * as angular from "angular";
2 | import { globalModule } from './global.module';
3 |
4 | /**
5 | * This service stores and retrieves user preferences in session storage
6 | */
7 | export class AppConfig {
8 | sort: string = '+date';
9 | emailAddress: string = undefined;
10 | restDelay: number = 100;
11 |
12 | constructor() {
13 | this.load();
14 | }
15 |
16 | load() {
17 | try {
18 | return angular.extend(this, angular.fromJson(sessionStorage.getItem("appConfig")))
19 | } catch (Error) { }
20 |
21 | return this;
22 | }
23 |
24 | save() {
25 | sessionStorage.setItem("appConfig", angular.toJson(angular.extend({}, this)));
26 | }
27 | }
28 |
29 | globalModule.service('AppConfig', AppConfig);
30 |
--------------------------------------------------------------------------------
/src/app/global/auth.service.ts:
--------------------------------------------------------------------------------
1 | import {AppConfig} from "./appConfig.service";
2 | import { globalModule } from './global.module';
3 |
4 | /**
5 | * This service emulates an Authentication Service.
6 | */
7 | export class AuthService {
8 | // data
9 | usernames: string[] = ['myself@angular.dev', 'devgal@angular.dev', 'devguy@angular.dev'];
10 |
11 | static $inject = ['AppConfig', '$q', '$timeout'];
12 | constructor(public AppConfig: AppConfig, public $q, public $timeout) { }
13 |
14 | /**
15 | * Returns true if the user is currently authenticated, else false
16 | */
17 | isAuthenticated() {
18 | return !!this.AppConfig.emailAddress;
19 | }
20 |
21 | /**
22 | * Fake authentication function that returns a promise that is either resolved or rejected.
23 | *
24 | * Given a username and password, checks that the username matches one of the known
25 | * usernames (this.usernames), and that the password matches 'password'.
26 | *
27 | * Delays 800ms to simulate an async REST API delay.
28 | */
29 | authenticate(username, password) {
30 | let { $timeout, $q, AppConfig } = this;
31 |
32 | // checks if the username is one of the known usernames, and the password is 'password'
33 | const checkCredentials = () => $q((resolve, reject) => {
34 | var validUsername = this.usernames.indexOf(username) !== -1;
35 | var validPassword = password === 'password';
36 |
37 | return (validUsername && validPassword) ? resolve(username) : reject("Invalid username or password");
38 | });
39 |
40 | return $timeout(checkCredentials, 800)
41 | .then((authenticatedUser) => {
42 | AppConfig.emailAddress = authenticatedUser;
43 | AppConfig.save()
44 | });
45 | }
46 |
47 | /** Logs the current user out */
48 | logout() {
49 | this.AppConfig.emailAddress = undefined;
50 | this.AppConfig.save();
51 | }
52 | }
53 |
54 | globalModule.service('AuthService', AuthService);
55 |
--------------------------------------------------------------------------------
/src/app/global/dataSources.service.ts:
--------------------------------------------------------------------------------
1 | import {SessionStorage} from "../util/sessionStorage"
2 | import {AppConfig} from "./appConfig.service";
3 | import { globalModule } from './global.module';
4 |
5 | /**
6 | * Fake REST Services (Contacts, Folders, Messages) used in the mymessages submodule.
7 | *
8 | * Each of these APIs have:
9 | *
10 | * .all()
11 | * .search(exampleItem)
12 | * .get(id)
13 | * .save(item)
14 | * .post(item)
15 | * .put(item)
16 | * .remove(item)
17 | *
18 | * See ../util/sessionStorage.js for more details, if curious
19 | */
20 |
21 | /** A fake Contacts REST client API */
22 | export class Contacts extends SessionStorage {
23 | static $inject = ['$http', '$timeout', '$q', 'AppConfig'];
24 | constructor($http, $timeout, $q, AppConfig: AppConfig) {
25 | // http://beta.json-generator.com/api/json/get/V1g6UwwGx
26 | super($http, $timeout, $q, "contacts", "data/contacts.json", AppConfig);
27 | }
28 | }
29 |
30 | /** A fake Folders REST client API */
31 | export class Folders extends SessionStorage {
32 | static $inject = ['$http', '$timeout', '$q', 'AppConfig'];
33 | constructor($http, $timeout, $q, AppConfig) {
34 | super($http, $timeout, $q, 'folders', 'data/folders.json', AppConfig);
35 | }
36 | }
37 |
38 | export class Messages extends SessionStorage {
39 | static $inject = ['$http', '$timeout', '$q', 'AppConfig'];
40 | constructor($http, $timeout, $q, public AppConfig: AppConfig) {
41 | // http://beta.json-generator.com/api/json/get/VJl5GbIze
42 | super($http, $timeout, $q, 'messages', 'data/messages.json', AppConfig);
43 | }
44 |
45 | byFolder(folder) {
46 | let searchObject = { folder: folder._id };
47 | let toFromAttr = ["drafts", "sent"].indexOf(folder._id) !== -1 ? "from" : "to";
48 | searchObject[toFromAttr] = this.AppConfig.emailAddress;
49 | return this.search(searchObject);
50 | }
51 | }
52 |
53 | globalModule.service('Contacts', Contacts);
54 | globalModule.service('Folders', Folders);
55 | globalModule.service('Messages', Messages);
56 |
--------------------------------------------------------------------------------
/src/app/global/dialog.directive.ts:
--------------------------------------------------------------------------------
1 | import { globalModule } from './global.module';
2 |
3 | dialogDirective.$inject = ['$timeout', '$q'];
4 | function dialogDirective($timeout, $q) {
5 | return {
6 | link: (scope, elem) => {
7 | $timeout(() => elem.addClass('active'));
8 | elem.data('promise', $q((resolve, reject) => {
9 | scope.yes = () => resolve(true);
10 | scope.no = () => reject(false);
11 | }));
12 | },
13 | template: `
14 |
15 |
16 |
17 |
{{message}}
18 |
{{details}}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | `
27 | }
28 | }
29 |
30 | globalModule.directive('dialog', dialogDirective);
31 |
--------------------------------------------------------------------------------
/src/app/global/dialog.service.ts:
--------------------------------------------------------------------------------
1 | import * as angular from "angular";
2 | import { globalModule } from './global.module';
3 |
4 | export class DialogService {
5 | confirm;
6 |
7 | static $inject = ['$document', '$compile', '$rootScope'];
8 | constructor($document, $compile, $rootScope) {
9 | let body = $document.find("body");
10 | let elem = angular.element("");
11 |
12 | this.confirm = (message, details = "Are you sure?", yesMsg = "Yes", noMsg = "No") => {
13 | $compile(elem)(angular.extend($rootScope.$new(), {message, details, yesMsg, noMsg}));
14 | body.append(elem);
15 | return elem.data("promise").finally(() => elem.removeClass('active').remove());
16 | }
17 | }
18 | }
19 |
20 | globalModule.service('DialogService', DialogService);
21 |
--------------------------------------------------------------------------------
/src/app/global/global.module.ts:
--------------------------------------------------------------------------------
1 | import * as angular from 'angular';
2 | export const globalModule = angular.module('global', ['ui.router']);
3 |
--------------------------------------------------------------------------------
/src/app/global/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./global.module";
2 |
3 | import "./appConfig.service";
4 | import "./auth.service";
5 | import "./dataSources.service";
6 | import "./dialog.directive";
7 | import "./dialog.service";
8 | import "./requiresAuth.hook";
9 |
--------------------------------------------------------------------------------
/src/app/global/requiresAuth.hook.ts:
--------------------------------------------------------------------------------
1 | import { globalModule } from './global.module';
2 |
3 | /**
4 | * This file contains a Transition Hook which protects a
5 | * route that requires authentication.
6 | *
7 | * This hook redirects to /login when both:
8 | * - The user is not authenticated
9 | * - The user is navigating to a state that requires authentication
10 | */
11 | authHookRunBlock.$inject = ['$transitions', 'AuthService'];
12 | function authHookRunBlock($transitions, AuthService) {
13 | // Matches if the destination state's data property has a truthy 'requiresAuth' property
14 | let requiresAuthCriteria = {
15 | to: (state) => state.data && state.data.requiresAuth
16 | };
17 |
18 | // Function that returns a redirect for the current transition to the login state
19 | // if the user is not currently authenticated (according to the AuthService)
20 |
21 | let redirectToLogin = (transition) => {
22 | let AuthService = transition.injector().get('AuthService');
23 | let $state = transition.router.stateService;
24 | if (!AuthService.isAuthenticated()) {
25 | return $state.target('login', undefined, { location: false });
26 | }
27 | };
28 |
29 | // Register the "requires auth" hook with the TransitionsService
30 | $transitions.onBefore(requiresAuthCriteria, redirectToLogin, {priority: 10});
31 | }
32 |
33 | globalModule.run(authHookRunBlock);
34 |
--------------------------------------------------------------------------------
/src/app/home/README.md:
--------------------------------------------------------------------------------
1 | ## Contents
2 |
3 | ### The main app module bootstrap
4 |
5 | - *app.states*.js: Defines the top-level states such as home, welcome, and login
6 |
7 | ### Components for the Top-level states
8 |
9 | - *app.component*.js: A component which displays the header nav-bar for authenticated in users
10 | - *home.component*.js: A component that has links to the main submodules
11 | - *login.component*.js: A component for authenticating a guest user
12 | - *welcome.component*.js: A component which displays a welcome screen for guest users
13 |
--------------------------------------------------------------------------------
/src/app/home/app.component.ts:
--------------------------------------------------------------------------------
1 | import { homeModule } from './home.module';
2 |
3 | /**
4 | * The controller for the `app` component.
5 | */
6 | class AuthedController {
7 | //data
8 | emailAddress;
9 | isAuthenticated;
10 |
11 | static $inject = ['AppConfig', 'AuthService', '$state'];
12 | constructor(AppConfig, public AuthService, public $state) {
13 | this.emailAddress = AppConfig.emailAddress;
14 | this.isAuthenticated = AuthService.isAuthenticated();
15 | }
16 |
17 | logout() {
18 | let {AuthService, $state} = this;
19 | AuthService.logout();
20 | // Reload states after authentication change
21 | return $state.go('welcome', {}, { reload: true });
22 | }
23 | }
24 |
25 | /**
26 | * This is the main app component for an authenticated user.
27 | *
28 | * This component renders the outermost chrome (application header and tabs, the compose and logout button)
29 | * It has a `ui-view` viewport for nested states to fill in.
30 | */
31 | const appComponent = {
32 | controller: AuthedController,
33 | template: `
34 |
57 |
58 |
59 | `
60 | };
61 |
62 | homeModule.component('app', appComponent);
63 |
--------------------------------------------------------------------------------
/src/app/home/app.states.ts:
--------------------------------------------------------------------------------
1 | import { homeModule } from './home.module';
2 |
3 | /**
4 | * This is the parent state for the entire application.
5 | *
6 | * This state's primary purposes are:
7 | * 1) Shows the outermost chrome (including the navigation and logout for authenticated users)
8 | * 2) Provide a viewport (ui-view) for a substate to plug into
9 | */
10 | const appState = {
11 | name: 'app',
12 | redirectTo: 'welcome',
13 | component: 'app'
14 | };
15 |
16 | /**
17 | * This is the 'welcome' state. It is the default state (as defined by app.js) if no other state
18 | * can be matched to the URL.
19 | */
20 | const welcomeState = {
21 | parent: 'app',
22 | name: 'welcome',
23 | url: '/welcome',
24 | component: 'welcome'
25 | };
26 |
27 |
28 | /**
29 | * This is a home screen for authenticated users.
30 | *
31 | * It shows giant buttons which activate their respective submodules: Messages, Contacts, Preferences
32 | */
33 | const homeState = {
34 | parent: 'app',
35 | name: 'home',
36 | url: '/home',
37 | component: 'home'
38 | };
39 |
40 |
41 | /**
42 | * This is the login state. It is activated when the user navigates to /login, or if a unauthenticated
43 | * user attempts to access a protected state (or substate) which requires authentication. (see routerhooks/requiresAuth.js)
44 | *
45 | * It shows a fake login dialog and prompts the user to authenticate. Once the user authenticates, it then
46 | * reactivates the state that the user originally came from.
47 | */
48 | const loginState = {
49 | parent: 'app',
50 | name: 'login',
51 | url: '/login',
52 | component: 'login',
53 | resolve: { returnTo: returnTo }
54 | };
55 |
56 | /**
57 | * A resolve function for 'login' state which figures out what state to return to, after a successful login.
58 | *
59 | * If the user was initially redirected to login state (due to the requiresAuth redirect), then return the toState/params
60 | * they were redirected from. Otherwise, if they transitioned directly, return the fromState/params. Otherwise
61 | * return the main "home" state.
62 | */
63 | returnTo.$inject = ['$transition$'];
64 | function returnTo ($transition$): any {
65 | if ($transition$.redirectedFrom() != null) {
66 | // The user was redirected to the login state (e.g., via the requiresAuth hook when trying to activate contacts)
67 | // Return to the original attempted target state (e.g., contacts)
68 | return $transition$.redirectedFrom().targetState();
69 | }
70 |
71 | let $state = $transition$.router.stateService;
72 |
73 | // The user was not redirected to the login state; they directly activated the login state somehow.
74 | // Return them to the state they came from.
75 | if ($transition$.from().name !== '') {
76 | return $state.target($transition$.from(), $transition$.params("from"));
77 | }
78 |
79 | // If the fromState's name is empty, then this was the initial transition. Just return them to the home state
80 | return $state.target('home');
81 | }
82 |
83 | homeModule.config(['$stateProvider', ($stateProvider) => {
84 | $stateProvider.state(appState);
85 | $stateProvider.state(welcomeState);
86 | $stateProvider.state(homeState);
87 | $stateProvider.state(loginState);
88 | }]);
89 |
--------------------------------------------------------------------------------
/src/app/home/home.component.ts:
--------------------------------------------------------------------------------
1 | import { homeModule } from './home.module';
2 |
3 | // This is a home component for authenticated users.
4 | // It shows giant buttons which activate their respective submodules: Messages, Contacts, Preferences
5 | const homeComponent = {
6 | template: `
7 |
8 |
12 |
13 |
17 |
18 |
22 |
23 | `};
24 |
25 | homeModule.component('home', homeComponent);
26 |
--------------------------------------------------------------------------------
/src/app/home/home.module.ts:
--------------------------------------------------------------------------------
1 | import * as angular from 'angular';
2 | export const homeModule = angular.module('main', ['ui.router']);
3 |
--------------------------------------------------------------------------------
/src/app/home/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./home.module";
2 |
3 | import "./app.component";
4 | import "./app.states";
5 | import "./home.component";
6 | import "./login.component";
7 | import "./welcome.component";
8 |
--------------------------------------------------------------------------------
/src/app/home/login.component.ts:
--------------------------------------------------------------------------------
1 | import * as angular from 'angular';
2 |
3 | import { homeModule } from './home.module';
4 | import { TargetState } from "@uirouter/core";
5 |
6 | /**
7 | * The controller for the `login` component
8 | *
9 | * The `login` method validates the credentials.
10 | * Then it sends the user back to the `returnTo` state, which is provided as a resolve data.
11 | */
12 | class LoginController {
13 | returnTo: TargetState;
14 |
15 | usernames;
16 | credentials;
17 | authenticating;
18 | errorMessage;
19 |
20 | login;
21 |
22 | static $inject = ['AppConfig', 'AuthService', '$state'];
23 | constructor(AppConfig, AuthService, $state) {
24 | this.usernames = AuthService.usernames;
25 |
26 | this.credentials = {
27 | username: AppConfig.emailAddress,
28 | password: 'password'
29 | };
30 |
31 | this.login = (credentials) => {
32 | this.authenticating = true;
33 |
34 | const returnToOriginalState = () => {
35 | let state = this.returnTo.state();
36 | let params = this.returnTo.params();
37 | let options = angular.extend({}, this.returnTo.options(), { reload: true });
38 | $state.go(state, params, options);
39 | };
40 |
41 | const showError = (errorMessage) =>
42 | this.errorMessage = errorMessage;
43 |
44 | AuthService.authenticate(credentials.username, credentials.password)
45 | .then(returnToOriginalState)
46 | .catch(showError)
47 | .finally(() => this.authenticating = false);
48 | }
49 | }
50 | }
51 |
52 | /**
53 | * This component renders a faux authentication UI
54 | *
55 | * It prompts for the username/password (and gives hints with bouncy arrows)
56 | * It shows errors if the authentication failed for any reason.
57 | */
58 | const loginComponent = {
59 | bindings: { returnTo: '<' },
60 |
61 | controller: LoginController,
62 |
63 | template: `
64 |
65 |
66 |
Log In
67 |
(This login screen is for demonstration only... just pick a username, enter 'password' and click "Log in")
68 |
69 |
70 |
71 |
72 |
74 | Choose
76 |
77 |
78 |
79 |
80 |
81 |
82 |
84 | Enter 'password' here
85 |
86 |
This is a demonstration app intended to highlight some patterns that can be used within UI-Router.
11 | These patterns should help you to to build cohesive, robust apps. Additionally, this app uses state-vis
12 | to show the tree of states, and a transition log visualizer.
13 |
14 |
App Overview
15 |
16 | First, start exploring the application's functionality at a high level by activating
17 | one of the three submodules: Messages, Contacts, or Preferences. If you are not already logged in,
18 | you will be taken to an authentication screen (the authentication is fake; the password is "password")
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Patterns and Recipes
27 |
28 |
Require Authentication
29 |
Previous State
30 |
Redirect Hook
31 |
Default Param Values
32 |
33 |
`
34 | };
35 |
36 | homeModule.component('welcome', welcomeComponent);
37 |
--------------------------------------------------------------------------------
/src/app/mymessages/README.md:
--------------------------------------------------------------------------------
1 | ## Contents
2 |
3 | ### States
4 |
5 | - *mymessages.states.ts*: The MyMessages states
6 |
7 | ### Components
8 |
9 | - *compose.component*.ts: Edits a new message
10 | - *folderList.component*.ts: Displays a list of folders.
11 | - *message.component*.ts: Displays the contents of a message.
12 | - *messageList.component*.ts: Displays a list of messages.
13 | - *messageTable.component*.ts: Displays a folder of messages as a table.
14 | - *mymessages.component*.ts: Displays a list of folders.
15 |
16 | ### Filters
17 |
18 | - *filters/messageBody.filter*.ts: Converts plain text formatting to something that html can display nicer.
19 |
20 | ### Directives
21 |
22 | - *directives/sortMessages*.js: A directive used in messageTable to toggle the currently sorted column
23 |
24 | ### Services
25 |
26 | - *services/messageListUI.service*.ts: A service used to find the closest (next/prev) message to the current message
27 |
--------------------------------------------------------------------------------
/src/app/mymessages/compose.component.ts:
--------------------------------------------------------------------------------
1 | import * as angular from "angular";
2 | import { mymessagesModule } from './mymessages.module';
3 |
4 | /**
5 | * The controller for the Compose component
6 | */
7 | class ComposeController {
8 | // bound
9 | $stateParams;
10 | $transition$;
11 |
12 | // data
13 | pristineMessage;
14 | message;
15 | canExit: boolean;
16 |
17 | static $inject = ['$state', 'DialogService', 'AppConfig', 'Messages'];
18 | constructor(public $state, public DialogService, public AppConfig, public Messages) { }
19 |
20 | /**
21 | * Create our message's model using the current user's email address as 'message.from'
22 | * Then extend it with all the properties from (non-url) state parameter 'message'.
23 | * Keep two copies: the editable one and the original one.
24 | * These copies are used to check if the message is dirty.
25 | */
26 | $onInit() {
27 | this.pristineMessage = angular.extend({from: this.AppConfig.emailAddress}, this.$stateParams.message);
28 | this.message = angular.copy(this.pristineMessage);
29 | }
30 |
31 | /**
32 | * Checks if the edited copy and the pristine copy are identical when the state is changing.
33 | * If they are not identical, the allows the user to confirm navigating away without saving.
34 | */
35 | uiCanExit() {
36 | if (this.canExit || angular.equals(this.pristineMessage, this.message)) {
37 | return true;
38 | }
39 |
40 | var message = 'You have not saved this message.';
41 | var question = 'Navigate away and lose changes?';
42 | return this.DialogService.confirm(message, question, "Yes", "No");
43 | }
44 |
45 | /**
46 | * Navigates back to the previous state.
47 | *
48 | * - Checks the $transition$ which activated this controller for a 'from state' that isn't the implicit root state.
49 | * - If there is no previous state (because the user deep-linked in, etc), then go to 'mymessages.messagelist'
50 | */
51 | gotoPreviousState() {
52 | let $transition$ = this.$transition$;
53 | let hasPrevious = !!$transition$.from().name;
54 | let state = hasPrevious ? $transition$.from() : "mymessages.messagelist";
55 | let params = hasPrevious ? $transition$.params("from") : {};
56 | this.$state.go(state, params);
57 | };
58 |
59 | /** "Send" the message (save to the 'sent' folder), and then go to the previous state */
60 | send(message) {
61 | this.Messages.save(angular.extend(message, {date: new Date(), read: true, folder: 'sent'}))
62 | .then(() => this.canExit = true)
63 | .then(() => this.gotoPreviousState());
64 | };
65 |
66 | /** Save the message to the 'drafts' folder, and then go to the previous state */
67 | save(message) {
68 | this.Messages.save(angular.extend(message, {date: new Date(), read: true, folder: 'drafts'}))
69 | .then(() => this.canExit = true)
70 | .then(() => this.gotoPreviousState());
71 | }
72 | }
73 |
74 | /**
75 | * This component composes a message
76 | *
77 | * The message might be new, a saved draft, or a reply/forward.
78 | * A Cancel button discards the new message and returns to the previous state.
79 | * A Save As Draft button saves the message to the "drafts" folder.
80 | * A Send button sends the message
81 | */
82 | const composeComponent = {
83 | bindings: { $stateParams: '<', $transition$: '<' },
84 |
85 | controller: ComposeController,
86 |
87 | template: `
88 |
134 | `};
135 |
136 | mymessagesModule.component('message', messageComponent);
137 |
--------------------------------------------------------------------------------
/src/app/mymessages/messageList.component.ts:
--------------------------------------------------------------------------------
1 | import { mymessagesModule } from './mymessages.module';
2 |
3 | /**
4 | * This component renders a list of messages using the `messageTable` component
5 | */
6 | const messageListComponent = {
7 | bindings: { folder: '<', messages: '<' },
8 | template: `
9 |
10 |
11 |
12 | `};
13 |
14 | mymessagesModule.component('messageList', messageListComponent);
--------------------------------------------------------------------------------
/src/app/mymessages/messageTable.component.ts:
--------------------------------------------------------------------------------
1 | import { mymessagesModule } from './mymessages.module';
2 |
3 | messageTableController.$inject = ['AppConfig'];
4 | function messageTableController(AppConfig) {
5 | this.AppConfig = AppConfig;
6 | this.colVisible = (name) => this.columns.indexOf(name) !== -1;
7 | }
8 |
9 | /**
10 | * A component that displays a folder of messages as a table
11 | *
12 | * If a row is clicked, the details of the message is shown using a relative ui-sref to `.message`.
13 | *
14 | * ui-sref-active is used to highlight the selected row.
15 | *
16 | * Shows/hides specific columns based on the `columns` input binding.
17 | */
18 | const messageTableComponent = {
19 | bindings: { columns: '<', messages: '<' },
20 |
21 | controller: messageTableController,
22 |
23 | template: `
24 |
25 |
26 |
27 |
28 |
Sender
29 |
Recipient
30 |
Subject
31 |
Date
32 |
33 |
34 |
35 |
36 |
38 |
39 |
{{ message.from }}
40 |
{{ message.to }}
41 |
{{ message.subject }}
42 |
{{ message.date | date: "yyyy-MM-dd" }}
43 |
44 |
45 |
46 |
47 | `};
48 |
49 | mymessagesModule.component('messageTable', messageTableComponent);
50 |
--------------------------------------------------------------------------------
/src/app/mymessages/mymessages.component.ts:
--------------------------------------------------------------------------------
1 | import { mymessagesModule } from './mymessages.module';
2 |
3 | /**
4 | * The main mymessages component.
5 | *
6 | * Renders a list of folders, and has two viewports:
7 | * - messageList: filled with the list of messages for a folder
8 | * - messagecontent: filled with the contents of a single message.
9 | */
10 | const mymessagesComponent = {
11 | bindings: {folders: '<'},
12 |
13 | template: `
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | `};
27 |
28 | mymessagesModule.component('mymessages', mymessagesComponent);
29 |
--------------------------------------------------------------------------------
/src/app/mymessages/mymessages.module.ts:
--------------------------------------------------------------------------------
1 | import * as angular from 'angular';
2 |
3 | // The angularjs module for 'mymessages'
4 | // This module is imported in each of the
5 | export const mymessagesModule = angular.module('mymessages', ['ui.router']);
6 |
--------------------------------------------------------------------------------
/src/app/mymessages/mymessages.states.ts:
--------------------------------------------------------------------------------
1 | import { mymessagesModule } from './mymessages.module';
2 |
3 | /**
4 | * This state allows the user to compose a new message, edit a drafted message, send a message,
5 | * or save an unsent message as a draft.
6 | *
7 | * This state uses view-targeting to take over the ui-view that would normally be filled by the 'mymessages' state.
8 | */
9 | const composeState = {
10 | name: 'mymessages.compose',
11 | url: '/compose',
12 | // Declares that this state has a 'message' parameter, that defaults to an empty object.
13 | // Note the parameter does not appear in the URL.
14 | params: {
15 | message: {}
16 | },
17 | views: {
18 | // Absolutely targets the $default (unnamed) ui-view, two nesting levels down with the composeComponent.
19 | "!$default.$default": 'compose'
20 | }
21 | };
22 |
23 | /**
24 | * The mymessages state. This is the main state for the mymessages submodule.
25 | *
26 | * This state shows the list of folders for the current user. It retrieves the folders from the
27 | * Folders service. If a user navigates directly to this state, the state redirects to the 'mymessages.messagelist'.
28 | */
29 | const mymessagesState = {
30 | parent: 'app',
31 | name: "mymessages",
32 | url: "/mymessages",
33 | resolve: {
34 | // All the folders are fetched from the Folders service
35 | folders: ['Folders', (Folders) => {
36 | return Folders.all();
37 | }],
38 | },
39 | // If mymessages state is directly activated, redirect the transition to the child state 'mymessages.messagelist'
40 | redirectTo: 'mymessages.messagelist',
41 | component: 'mymessages',
42 | // Mark this state as requiring authentication. See ../routerhooks/requiresAuth.js.
43 | data: { requiresAuth: true }
44 | };
45 |
46 |
47 | /**
48 | * This state shows the contents of a single message.
49 | * It also has UI to reply, forward, delete, or edit an existing draft.
50 | */
51 | const messageState = {
52 | name: 'mymessages.messagelist.message',
53 | url: '/:messageId',
54 | resolve: {
55 | // Fetch the message from the Messages service using the messageId parameter
56 | message: ['Messages', '$stateParams', (Messages, $stateParams) => {
57 | return Messages.get($stateParams.messageId);
58 | }],
59 | // Provide the component with a function it can query that returns the closest message id
60 | nextMessageGetter: ['MessageListUI', 'messages', (MessageListUI, messages) => {
61 | return MessageListUI.proximalMessageId.bind(MessageListUI, messages);
62 | }],
63 | },
64 | views: {
65 | // Relatively target the parent-state's parent-state's 'messagecontent' ui-view
66 | // This could also have been written using ui-view@state addressing: 'messagecontent@mymessages'
67 | // Or, this could also have been written using absolute ui-view addressing: '!$default.$default.messagecontent'
68 | "^.^.messagecontent": 'message'
69 | }
70 | };
71 |
72 |
73 | /**
74 | * This state shows the contents (a message list) of a single folder
75 | */
76 | const messageListState = {
77 | name: 'mymessages.messagelist',
78 | url: '/:folderId',
79 | // The folderId parameter is part of the URL. This params block sets 'inbox' as the default value.
80 | // If no parameter value for folderId is provided on the transition, then it will be defaulted to 'inbox'
81 | params: {folderId: "inbox"},
82 | resolve: {
83 | // Fetch the current folder from the Folders service, using the folderId parameter
84 | folder: ['Folders', '$stateParams', (Folders, $stateParams) => {
85 | return Folders.get($stateParams.folderId);
86 | }],
87 |
88 | // The resolved folder object (from the resolve above) is injected into this resolve
89 | // The list of message for the folder are fetched from the Messages service
90 | messages: ['Messages', 'folder', (Messages, folder) => {
91 | return Messages.byFolder(folder);
92 | }],
93 | },
94 | views: {
95 | // This targets the "messagelist" named ui-view added to the DOM in the parent state 'mymessages'
96 | "messagelist": 'messageList'
97 | }
98 | };
99 |
100 | mymessagesModule.config(['$stateProvider', ($stateProvider) => {
101 | $stateProvider.state(composeState);
102 | $stateProvider.state(mymessagesState);
103 | $stateProvider.state(messageState);
104 | $stateProvider.state(messageListState);
105 | }]);
106 |
--------------------------------------------------------------------------------
/src/app/mymessages/services/index.ts:
--------------------------------------------------------------------------------
1 | import "./messagesListUI.service";
--------------------------------------------------------------------------------
/src/app/mymessages/services/messagesListUI.service.ts:
--------------------------------------------------------------------------------
1 | import { mymessagesModule } from '../mymessages.module';
2 |
3 | /** Provides services related to a message list */
4 | class MessageListUI {
5 | static $inject = ['$filter', 'AppConfig'];
6 | constructor(public $filter, public AppConfig) { }
7 |
8 | /** This is a UI helper which finds the nearest messageId in the messages list to the messageId parameter */
9 | proximalMessageId(messages, messageId) {
10 | let sorted = this.$filter("orderBy")(messages, this.AppConfig.sort);
11 | let idx = sorted.findIndex(msg => msg._id === messageId);
12 | var proximalIdx = sorted.length > idx + 1 ? idx + 1 : idx - 1;
13 | return proximalIdx >= 0 ? sorted[proximalIdx]._id : undefined;
14 | }
15 | }
16 |
17 | mymessagesModule.service('MessageListUI', MessageListUI);
18 |
--------------------------------------------------------------------------------
/src/app/prefs/README.md:
--------------------------------------------------------------------------------
1 | This is an Angular module which is imported into the bootstrapped Angular Module
2 |
3 | ## Contents
4 |
5 | ### Module
6 |
7 | - *prefs.module*.js: Defines the Angular Module for the Prefs feature module
8 |
9 | ### States
10 |
11 | - *prefs.state*.ts: A template/controller for showing and/or updating user preferences.
12 |
13 | ### Components
14 |
15 | - *prefs.component*.ts: Displays and updates user preferences.
16 |
--------------------------------------------------------------------------------
/src/app/prefs/prefs.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Inject } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'prefs-component',
5 | template: `
6 |