├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── favicon.ico
├── gh-pages-patch.diff
├── karma.config.js
├── package-lock.json
├── package.json
├── protractor.config.js
├── publish-to-gh-pages.sh
├── source
├── app
│ ├── components
│ │ ├── _common
│ │ │ ├── config.js
│ │ │ ├── directives
│ │ │ │ ├── datepicker-init.directive.js
│ │ │ │ ├── datepicker-init.directive.spec.js
│ │ │ │ ├── dropdown-init.directive.js
│ │ │ │ ├── dropdown-init.directive.spec.js
│ │ │ │ ├── focus-me.directive.js
│ │ │ │ ├── focus-me.directive.spec.js
│ │ │ │ ├── loading-button.directive.js
│ │ │ │ ├── loading-button.directive.spec.js
│ │ │ │ ├── select-init.directive.js
│ │ │ │ ├── select-init.directive.spec.js
│ │ │ │ ├── tooltip-init.directive.js
│ │ │ │ ├── tooltip-init.directive.spec.js
│ │ │ │ ├── validate-number.directive.js
│ │ │ │ └── validate-number.directive.spec.js
│ │ │ ├── index.js
│ │ │ ├── production
│ │ │ │ ├── exception-handler.decorator.js
│ │ │ │ ├── exception-handler.decorator.spec.js
│ │ │ │ └── production.config.js
│ │ │ ├── run.js
│ │ │ ├── services
│ │ │ │ ├── ajax-error-handler.service.js
│ │ │ │ ├── ajax-error-handler.service.spec.js
│ │ │ │ ├── error.service.js
│ │ │ │ ├── error.service.spec.js
│ │ │ │ ├── event.constant.js
│ │ │ │ ├── event.constant.spec.js
│ │ │ │ ├── logger.service.js
│ │ │ │ ├── logger.service.spec.js
│ │ │ │ ├── resolve.service.js
│ │ │ │ ├── resolve.service.spec.js
│ │ │ │ ├── router-helper.provider.js
│ │ │ │ ├── router-helper.provider.spec.js
│ │ │ │ ├── user.service.js
│ │ │ │ └── user.service.spec.js
│ │ │ └── styles
│ │ │ │ ├── animation.styl
│ │ │ │ ├── common.styl
│ │ │ │ ├── fonts.styl
│ │ │ │ ├── helper.styl
│ │ │ │ ├── mixin.styl
│ │ │ │ ├── responsive.styl
│ │ │ │ └── variable.styl
│ │ ├── _layout
│ │ │ ├── bg.png
│ │ │ ├── index.js
│ │ │ ├── layout.route.js
│ │ │ ├── layout.styl
│ │ │ ├── logo.png
│ │ │ └── main.layout.jade
│ │ ├── banner
│ │ │ ├── banner.directive.js
│ │ │ ├── banner.directive.spec.js
│ │ │ ├── banner.jade
│ │ │ └── index.js
│ │ ├── breadcrumb
│ │ │ ├── breadcrumb.controller.js
│ │ │ ├── breadcrumb.controller.spec.js
│ │ │ ├── breadcrumb.jade
│ │ │ ├── breadcrumb.styl
│ │ │ └── index.js
│ │ ├── footer
│ │ │ ├── footer.controller.js
│ │ │ ├── footer.controller.spec.js
│ │ │ ├── footer.jade
│ │ │ ├── footer.styl
│ │ │ └── index.js
│ │ ├── header
│ │ │ ├── header.controller.js
│ │ │ ├── header.controller.spec.js
│ │ │ ├── header.jade
│ │ │ ├── header.styl
│ │ │ └── index.js
│ │ ├── home-hero
│ │ │ ├── home-hero.directive.js
│ │ │ ├── home-hero.directive.spec.js
│ │ │ ├── home-hero.jade
│ │ │ ├── home-hero.styl
│ │ │ └── index.js
│ │ ├── loading
│ │ │ ├── config.js
│ │ │ ├── index.js
│ │ │ └── loading.styl
│ │ ├── login-form
│ │ │ ├── index.js
│ │ │ ├── login-form.controller.js
│ │ │ ├── login-form.controller.spec.js
│ │ │ ├── login-form.directive.js
│ │ │ ├── login-form.jade
│ │ │ └── login-form.styl
│ │ ├── modal
│ │ │ ├── index.js
│ │ │ ├── modal.service.js
│ │ │ ├── modal.service.spec.js
│ │ │ └── modal.styl
│ │ ├── phone-form
│ │ │ ├── index.js
│ │ │ ├── phone-form.controller.js
│ │ │ ├── phone-form.controller.spec.js
│ │ │ ├── phone-form.directive.js
│ │ │ ├── phone-form.jade
│ │ │ └── phone-form.styl
│ │ ├── phone-table
│ │ │ ├── index.js
│ │ │ ├── phone-table.controller.js
│ │ │ ├── phone-table.directive.js
│ │ │ ├── phone-table.directive.spec.js
│ │ │ ├── phone-table.jade
│ │ │ └── phone-table.styl
│ │ ├── sidebar-sm
│ │ │ ├── index.js
│ │ │ ├── sidebar-sm.controller.js
│ │ │ ├── sidebar-sm.controller.spec.js
│ │ │ ├── sidebar-sm.jade
│ │ │ └── sidebar-sm.styl
│ │ ├── sidebar
│ │ │ ├── index.js
│ │ │ ├── sidebar.controller.js
│ │ │ ├── sidebar.controller.spec.js
│ │ │ ├── sidebar.jade
│ │ │ └── sidebar.styl
│ │ └── square-menu
│ │ │ ├── index.js
│ │ │ ├── square-menu.directive.js
│ │ │ ├── square-menu.directive.spec.js
│ │ │ ├── square-menu.jade
│ │ │ └── square-menu.styl
│ ├── index.jade
│ ├── index.js
│ └── pages
│ │ ├── 404
│ │ ├── 404.jade
│ │ ├── 404.route.js
│ │ └── index.js
│ │ ├── dashboard
│ │ ├── dashboard.controller.js
│ │ ├── dashboard.controller.spec.js
│ │ ├── dashboard.jade
│ │ ├── dashboard.route.js
│ │ ├── dashboard.styl
│ │ └── index.js
│ │ ├── home
│ │ ├── home.jade
│ │ ├── home.route.js
│ │ ├── home.styl
│ │ └── index.js
│ │ ├── login
│ │ ├── index.js
│ │ ├── login.controller.js
│ │ ├── login.controller.spec.js
│ │ ├── login.jade
│ │ ├── login.route.js
│ │ └── login.styl
│ │ └── phone
│ │ ├── add
│ │ ├── phone-add.controller.js
│ │ ├── phone-add.controller.spec.js
│ │ ├── phone-add.jade
│ │ └── phone-add.styl
│ │ ├── detail
│ │ ├── phone-detail.controller.js
│ │ ├── phone-detail.controller.spec.js
│ │ ├── phone-detail.jade
│ │ └── phone-detail.styl
│ │ ├── index.js
│ │ ├── phone.controller.js
│ │ ├── phone.controller.spec.js
│ │ ├── phone.jade
│ │ ├── phone.route.js
│ │ ├── phone.service.js
│ │ └── phone.service.spec.js
└── test
│ ├── e2e
│ ├── helper.js
│ ├── mocks
│ │ ├── e2e.config.js
│ │ ├── e2e.data.js
│ │ ├── e2e.phone.js
│ │ ├── e2e.user.js
│ │ └── index.js
│ └── specs
│ │ ├── 404.spec.js
│ │ ├── dashboard.spec.js
│ │ ├── home.spec.js
│ │ ├── login.spec.js
│ │ ├── page-objects
│ │ ├── 404.page.js
│ │ ├── dashboard.page.js
│ │ ├── home.page.js
│ │ ├── login.page.js
│ │ ├── phone-add.page.js
│ │ ├── phone-detail.page.js
│ │ ├── phone-form.comp.js
│ │ └── phone-main.page.js
│ │ ├── phone-add.spec.js
│ │ ├── phone-detail.spec.js
│ │ └── phone-main.spec.js
│ └── unit
│ └── helper.js
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 4
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [package.json]
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | webpack.config.js
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # common
2 | .DS_Store
3 | # node dependencies
4 | node_modules
5 | npm-debug.log
6 |
7 | # build file
8 | build
9 |
10 | # test folder
11 | source/test/e2e/screenshots/
12 | source/test/unit/results/
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | branches:
2 | only:
3 | - master
4 | language: node_js
5 | node_js:
6 | - stable
7 | script:
8 | - ./publish-to-gh-pages.sh
9 | after_success:
10 | - npm run codecov
11 | - npm run server
12 | - npm run e2e -- --ci --build-id=$TRAVIS_BUILD_NUMBER --job-id=$TRAVIS_JOB_NUMBER
13 | env:
14 | global:
15 | - GitHub_REF: github.com/PinkyJie/angular1-webpack-starter.git
16 | - secure: "Sq6NJGdO4trRXiY+pr8RMV8GuZjsML8If6653ZKVqsHPFPS8eTEKNlIepXcWf/+I6cqVhiT0B/IUxVwdbw/Jh+0BLM1RxofvyFAcZAQaxhRnfDnVdGqysP89RlxuoR/Bc58F7qFwu4nfJ96dFfiIDEnV5whLbvekXperPeLDy41BbgRov4H2ifz3OiwAK2DISmM4ikJBCkwKOA0BHo9/cDtobHNicCM7sE36aYIjYXK6esM8aBZ/XHiLzF/rlfRGQFvNmaxq6Wu41GbtnatDg3PUqBIxCiutKpqEpPothGN/gbTZFNQBCJGmNqnp8KxD2ua3mBvmnL0x+Rij27uMFTfSqfDyLTx/A9M1tMYaIbwBNXz3tR7jyQk0WyAFbuTyw4i6rtM4qqRLSbkF3DXImM5qPqO5zlXNMSAAlOqDXWL9+PpU2U0S7YtxZiV5fzGuZ77F1XTsnHhTgNDru0l0mZhPzTfb+rnagcT74O/6XBeShEZ+eh9gfDLF0/uyXP4Jjif/7204M+Attk/GUuXht+A3+SttUJ/nGbvNJyUi1OXZonuRJSkEPvNu/88KyMvLP/QxTWo0UVE+2Vxb7p9MQo5Fl/W0cCTLQorJX/DYVkJErnktzHX0UV4Y0dR0pQKZpUcmkxkk2zQ/uLugVRD3oHw/Hnzbb3PWlPL5vosIsoE="
17 | - CXX: g++-4.8
18 | addons:
19 | sauce_connect:
20 | username: "sd4399340"
21 | access_key: "5829a37c-41c0-4490-b6c2-061ae4acc5e9"
22 | apt:
23 | sources:
24 | - ubuntu-toolchain-r-test
25 | packages:
26 | - gcc-4.8
27 | - g++-4.8
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 马斯特
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # angular1-webpack-starter
2 | [](https://travis-ci.org/PinkyJie/angular1-webpack-starter)
3 | [](https://codecov.io/github/PinkyJie/angular1-webpack-starter)
4 | [](https://david-dm.org/pinkyjie/angular1-webpack-starter#info=dependencies&view=table)
5 | [](https://david-dm.org/pinkyjie/angular1-webpack-starter#info=devDependencies&view=table)
6 | [](https://nodejs.org)
7 | [](https://www.npmjs.com/package/angular1-webpack-starter)
8 |
9 | A starter project using Angular 1.x with Webpack. A Webpack + ES6 re-implementation of the [generator-aio-angular](https://github.com/PinkyJie/generator-aio-angular) project.
10 |
11 | Still wanna use **Gulp + ES5**? Check the [generator-aio-angular](https://github.com/PinkyJie/generator-aio-angular) project.
12 |
13 | > Pure front-end implementation, all API interaction are mocked using [angular-mocks](https://docs.angularjs.org/api/ngMock).
14 |
15 | ## Preview
16 |
17 | Check out the [demo site](http://pinkyjie.com/angular1-webpack-starter/#/).
18 |
19 | > The dome site is a pure front-end implementation, so you can use any email/password to login, see [mock file](source/test/e2e/mocks/e2e.user.js) for detail. It is hosted on Github pages, no back-end support, so we use `#` style URL.
20 |
21 | ## Features
22 |
23 | * ES6
24 | * Component based structure proposed in https://github.com/fouber/blog/issues/10
25 | * Lazy load resources(js/css/images/templates...) for each page
26 | * Material Design using [MaterializeCSS](http://materializecss.com/)
27 | * Flex Layout
28 | * Responsive Design
29 | * Support multiple devices with different screen size.
30 | * Easy responsive implementation, very convenient to support small screen devices. (see [responsive.styl](source/app/components/_common/styles/responsive.styl))
31 | * Animation
32 | * Using [animate.css](https://daneden.github.io/animate.css/).
33 | * All the animation defined by `animate.css` can be used directly as keyframe animation. (see [animation.styl](source/app/components/_common/styles/animation.styl))
34 | * More understandable router design
35 | * Easy implementation for Sidebar Navigation and Breadcrumb
36 |
37 | ## Get Started
38 |
39 | ```bash
40 | git clone https://github.com/PinkyJie/angular1-webpack-starter.git
41 | cd angular1-webpack-starter
42 | npm install
43 | npm start
44 | ```
45 |
46 | Then open your browser with URL `http://localhost:8080/webpack-dev-server/`.
47 |
48 | ## Tests
49 |
50 | * Unit Test: `npm test`
51 | * Unit Test with auto watch: `npm run test:watch`
52 | * E2E Test: `npm run e2e`
53 | * run `npm run webdriver-update` first
54 | * make sure a local mock server is running
55 |
56 | Check the [Unit test coverage report](http://pinkyjie.com/angular1-webpack-starter/coverage).
57 |
58 | Check the E2E test report: [](https://saucelabs.com/u/sd4399340)
59 |
60 | [](https://saucelabs.com/u/sd4399340)
61 |
62 |
63 | ## Building
64 |
65 | ```bash
66 | npm run build
67 | ```
68 | The optimized files will be generated in `build` folder.
69 |
70 | ## CI
71 | Proudly use [Travis](https://travis-ci.org/) to do Continuous Integration.
72 |
73 | Every push will trigger a build on Travis, it will automatically:
74 | - run unit test.
75 | - run build script, deploy website and test coverage report to Github pages.
76 | - run E2E test on different browsers using [Sauce Labs](https://saucelabs.com).
77 |
78 | Check [.travis.yml](.travis.yml) and [publish-to-gh-pages.sh](publish-to-gh-pages.sh) for detail implementation.
79 |
80 | Check [Travis build log](https://travis-ci.org/PinkyJie/angular1-webpack-starter) for build results.
81 |
82 | ## Blog series
83 |
84 | http://pinkyjie.com/tags/angular1-webpack-starter/
85 |
86 | ## License
87 |
88 | MIT
89 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PinkyJie/angular1-webpack-starter/6e7b0f6fd04a94df996dfeecc447f38b167a5196/favicon.ico
--------------------------------------------------------------------------------
/gh-pages-patch.diff:
--------------------------------------------------------------------------------
1 | diff --git a/source/app/components/_common/services/router-helper.provider.js b/source/app/components/_common/services/router-helper.provider.js
2 | index 426cf72..5319d32 100644
3 | --- a/source/app/components/_common/services/router-helper.provider.js
4 | +++ b/source/app/components/_common/services/router-helper.provider.js
5 | @@ -101,7 +101,7 @@ class RouterHelperProvider {
6 | mainTitle: '',
7 | resolveAlways: {}
8 | };
9 | - this.$locationProvider.html5Mode(true);
10 | + this.$locationProvider.html5Mode(false);
11 | }
12 |
13 | configure (cfg) {
14 | diff --git a/source/app/index.jade b/source/app/index.jade
15 | index edbe21b..f16f9ab 100644
16 | --- a/source/app/index.jade
17 | +++ b/source/app/index.jade
18 | @@ -10,7 +10,7 @@ html(lang="en", ng-app= app, ng-strict-di)
19 | meta(charset="utf-8")
20 | meta(http-equiv="X-UA-Compatible", content="IE=edge, chrome=1")
21 | meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no")
22 | - base(href="/")
23 | + base(href="./")
24 | //- babel polyfill for IE
25 | //- conditional comment is not supported from IE 10
26 | //- TODO: how to include this only for IE
27 |
--------------------------------------------------------------------------------
/karma.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const args = require('yargs').argv;
4 |
5 | const unitTestEntry = 'source/test/unit/helper.js';
6 | // run multiple times in watch mode
7 | const singleRun = !args.watch;
8 | // use phantomjs in watch mode
9 | const browser = (args.watch || args.ci) ? 'PhantomJS' : 'Chrome';
10 | // load babel polyfill for phantomjs
11 | const files = browser === 'PhantomJS' ? [
12 | 'node_modules/babel-core/browser-polyfill.js',
13 | unitTestEntry
14 | ] : [
15 | unitTestEntry
16 | ];
17 |
18 | const include = [
19 | path.resolve('./source')
20 | ];
21 |
22 | const preLoaders = [
23 | // Process test code with Babel
24 | {test: /\.spec\.js$/, loader: 'babel', include},
25 | // Process all non-test code with Isparta
26 | {test: /\.js$/, loader: 'isparta', include, exclude: /\.spec\.js$/}
27 | ];
28 | const loaders = [
29 | {test: /\.styl$/, loader: 'style!css!stylus'},
30 | {test: /\.jade$/, loader: 'jade'},
31 | {test: /\.(png|jpg)$/, loader: 'null'}
32 | ];
33 | const processors = {};
34 | processors[unitTestEntry] = ['webpack', 'sourcemap'];
35 | processors['source/app/**/*.js'] = ['webpack', 'sourcemap'];
36 |
37 | // for watch mode, only show text coverage
38 | const reporters = args.watch ? [
39 | 'mocha', 'coverage'
40 | ] : [
41 | 'mocha', 'coverage', 'junit'
42 | ];
43 | const coverageReporters = args.watch ? [
44 | {type: 'text-summary'}
45 | ] : [
46 | {type: 'lcov', subdir: '.'},
47 | {type: 'text-summary'}
48 | ];
49 |
50 | module.exports = (config) => {
51 | config.set({
52 | basePath: '.',
53 | frameworks: ['jasmine'],
54 | exclude: [],
55 | files,
56 | webpack: {
57 | devtool: 'inline-source-map',
58 | module: {
59 | preLoaders,
60 | loaders
61 | },
62 | cache: true,
63 | plugins: [
64 | new webpack.ProvidePlugin({
65 | $: 'jquery',
66 | jQuery: 'jquery',
67 | 'window.jQuery': 'jquery'
68 | })
69 | ]
70 | },
71 | webpackMiddleware: {
72 | stats: {
73 | chunkModules: false,
74 | colors: true
75 | }
76 | },
77 | preprocessors: processors,
78 | reporters,
79 | coverageReporter: {
80 | dir: 'source/test/unit/results/coverage',
81 | reporters: coverageReporters
82 | },
83 | junitReporter: {
84 | outputDir: 'source/test/unit/results/junit'
85 | },
86 | reportSlowerThan: 500,
87 | singleRun,
88 | browsers: [
89 | browser
90 | ]
91 | });
92 | };
93 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular1-webpack-starter",
3 | "version": "0.5.0",
4 | "description": "Component based Angular web development with Webpack.",
5 | "engines": {
6 | "node": ">=4.0.0"
7 | },
8 | "repository": "PinkyJie/angular1-webpack-starter",
9 | "license": "MIT",
10 | "main": "source/app/index.js",
11 | "scripts": {
12 | "start": "webpack-dev-server --mock",
13 | "build": "rm -rf ./build && webpack --progress --mock --prod",
14 | "test": "rm -rf ./source/test/unit/results && karma start ./karma.config.js",
15 | "codecov": "cat ./source/test/unit/results/coverage/lcov.info | codecov",
16 | "test:watch": "karma start ./karma.config.js --watch",
17 | "webdriver-update": "webdriver-manager update",
18 | "webdriver-start": "webdriver-manager start",
19 | "e2e": "protractor ./protractor.config.js",
20 | "server": "nohup http-server ./build &"
21 | },
22 | "author": {
23 | "name": "马斯特",
24 | "email": "pilixiaoxuanfeng@gmail.com",
25 | "url": "https://github.com/PinkyJie"
26 | },
27 | "files": [
28 | "source"
29 | ],
30 | "keywords": [
31 | "es6",
32 | "components",
33 | "angular",
34 | "webpack",
35 | "jade",
36 | "stylus",
37 | "ui-router",
38 | "material design"
39 | ],
40 | "devDependencies": {
41 | "angular-mocks": "~1.4.5",
42 | "autoprefixer": "^6.3.3",
43 | "babel-core": "^5.8.24",
44 | "babel-loader": "^5.3.2",
45 | "codecov.io": "^0.1.6",
46 | "copy-webpack-plugin": "^0.3.3",
47 | "css-loader": "^0.17.0",
48 | "eslint": "^1.7.1",
49 | "eslint-loader": "^1.0.0",
50 | "extract-text-webpack-plugin": "^0.9.1",
51 | "file-loader": "^0.8.4",
52 | "html-webpack-plugin": "^2.9.0",
53 | "http-server": "^0.8.5",
54 | "isparta-loader": "^1.0.0",
55 | "jade": "^1.11.0",
56 | "jade-loader": "^0.7.1",
57 | "jasmine-core": "^2.3.4",
58 | "jasmine-spec-reporter": "^2.4.0",
59 | "karma": "^0.13.10",
60 | "karma-chrome-launcher": "^0.1.8",
61 | "karma-cli": "^0.1.1",
62 | "karma-coverage": "^0.3.1",
63 | "karma-jasmine": "^0.3.5",
64 | "karma-junit-reporter": "^0.3.6",
65 | "karma-mocha-reporter": "^1.0.2",
66 | "karma-phantomjs-launcher": "^0.2.1",
67 | "karma-sourcemap-loader": "^0.3.5",
68 | "karma-webpack": "^1.7.0",
69 | "null-loader": "^0.1.1",
70 | "oclazyload": "^1.0.8",
71 | "phantomjs": "^1.9.16",
72 | "postcss-loader": "^0.8.1",
73 | "protractor": "^5.1.2",
74 | "protractor-jasmine2-screenshot-reporter": "^0.1.7",
75 | "raw-loader": "^0.5.1",
76 | "style-loader": "^0.18.2",
77 | "stylus": "^0.54.5",
78 | "stylus-loader": "^3.0.1",
79 | "url-loader": "^0.5.6",
80 | "webpack": "^1.12.2",
81 | "webpack-dev-server": "^1.10.1",
82 | "yargs": "^3.25.0"
83 | },
84 | "dependencies": {
85 | "angular": "~1.4.5",
86 | "angular-animate": "~1.4.5",
87 | "angular-loading-bar": "^0.8.0",
88 | "angular-messages": "~1.4.5",
89 | "angular-ui-router": "^0.2.15",
90 | "animate.css": "^3.4.0",
91 | "hammerjs": "^2.0.4",
92 | "jquery": "~2.1.4",
93 | "materialize-css": "0.97.1"
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/publish-to-gh-pages.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -ev
3 |
4 | # run unit test (--ci: use phantomjs browser)
5 | npm test -- --ci
6 |
7 | # apply /#/ style router for SPA
8 | git apply ./gh-pages-patch.diff
9 |
10 | # run build
11 | npm run build
12 |
13 | # copy unit test coverage report to build folder
14 | cp -r ./source/test/unit/results/coverage/lcov-report ./build/coverage
15 | # rename _common to common cause github pages dose not support this
16 | mv ./build/coverage/app/components/_common ./build/coverage/app/components/common
17 | sed -i 's/href=\"app\/components\/_common/href=\"app\/components\/common/g' ./build/coverage/index.html
18 |
19 | # push build folder to github
20 | cd build
21 | # git init
22 | git init
23 | # inside this git repo we'll pretend to be a new user
24 | git config user.name "Travis CI"
25 | git config user.email "travis@pinkyjie.com"
26 |
27 | # The first and only commit to this new Git repo contains all the
28 | # files present with the commit message "Deploy to GitHub Pages".
29 | git add .
30 | git commit -m "Deploy at `date +"%Y-%m-%d %H:%M"`"
31 |
32 | # Force push from the current repo's master branch to the remote
33 | # repo's gh-pages branch. (All previous history on the gh-pages branch
34 | # will be lost, since we are overwriting it.) We redirect any output to
35 | # /dev/null to hide any sensitive credential data that might otherwise be exposed.
36 | git push --force --quiet "https://${GitHub_TOKEN}@${GitHub_REF}" master:gh-pages > /dev/null 2>&1
37 |
38 |
--------------------------------------------------------------------------------
/source/app/components/_common/config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | appTitle: 'Aio Angular App'
3 | };
4 |
5 | appConfig.$inject = ['RouterHelperProvider'];
6 | function appConfig (RouterHelperProvider) {
7 | RouterHelperProvider.configure({mainTitle: config.appTitle});
8 | }
9 |
10 | export default appConfig;
11 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/datepicker-init.directive.js:
--------------------------------------------------------------------------------
1 | function DatepickerInitDirective () {
2 | return {
3 | require: 'ngModel',
4 | restrict: 'A',
5 | link
6 | };
7 |
8 | function link (scope, element, attrs, ngModelCtrl) {
9 | const pickadate = element.pickadate({
10 | format: 'yyyy-m-d'
11 | });
12 | const watcher = scope.$watch(attrs.ngModel, (value) => {
13 | // set initial model value to date picker, only once
14 | if (value) {
15 | const picker = pickadate.pickadate('picker');
16 | picker.set('select', value);
17 | // cancel the watcher
18 | watcher();
19 | }
20 | });
21 | ngModelCtrl.$formatters.unshift((modelValue) => {
22 | if (modelValue) {
23 | const date = new Date(modelValue);
24 | return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
25 | }
26 | });
27 | }
28 | }
29 |
30 | DatepickerInitDirective.$inject = [];
31 |
32 | export default DatepickerInitDirective;
33 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/datepicker-init.directive.spec.js:
--------------------------------------------------------------------------------
1 | import DatepickerInitDirective from './datepicker-init.directive';
2 |
3 | describe('DatepickerInit Directive', () => {
4 | let scope;
5 | let element;
6 | let $compile;
7 |
8 | beforeEach(() => {
9 | angular.module('test', [])
10 | .directive('aioDatepickerInit', DatepickerInitDirective);
11 | angular.mock.module('test');
12 | });
13 |
14 | beforeEach(() => {
15 | angular.mock.inject(($rootScope, _$compile_) => {
16 | scope = $rootScope.$new();
17 | $compile = _$compile_;
18 | });
19 | });
20 |
21 | describe('Pass a valid date as ng-model', () => {
22 | beforeEach(() => {
23 | // 2015-10-1
24 | scope.date = new Date(2015, 9, 1);
25 | // special method to test jquery plugin function
26 | spyOn($.fn, 'pickadate').and.callThrough();
27 | element = $compile('')(scope);
28 | scope.$digest();
29 | });
30 |
31 | it('should call pickadate twice when initializing with valid date', () => {
32 | expect($.fn.pickadate).toHaveBeenCalledWith({format: 'yyyy-m-d'});
33 | // this is in watch function
34 | expect($.fn.pickadate).toHaveBeenCalledWith('picker');
35 | expect($.fn.pickadate.calls.count()).toEqual(2);
36 | });
37 |
38 | it('should not call pickadate any more even if model changes', () => {
39 | // change model but watch function will not be called again
40 | scope.date = new Date(2016, 10, 1);
41 | scope.$digest();
42 | expect($.fn.pickadate.calls.count()).toEqual(2);
43 | });
44 |
45 | it('should display the model in correct format', () => {
46 | expect(element.val()).toEqual('2015-10-1');
47 | });
48 | });
49 |
50 | describe('Pass a null/undefined value as ng-model', () => {
51 | beforeEach(() => {
52 | scope.date = null;
53 | // special method to test jquery plugin function
54 | spyOn($.fn, 'pickadate').and.callThrough();
55 | element = $compile('')(scope);
56 | scope.$digest();
57 | });
58 |
59 | it('should only call pickadate once when initializing with null', () => {
60 | expect($.fn.pickadate.calls.count()).toEqual(1);
61 | });
62 |
63 | it('should display nothing', () => {
64 | expect(element.val()).toEqual('');
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/dropdown-init.directive.js:
--------------------------------------------------------------------------------
1 | function DropdownInitDirective () {
2 | return {
3 | restrict: 'A',
4 | link
5 | };
6 |
7 | function link (scope, element) {
8 | element.dropdown();
9 | }
10 | }
11 |
12 | DropdownInitDirective.$inject = [];
13 |
14 | export default DropdownInitDirective;
15 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/dropdown-init.directive.spec.js:
--------------------------------------------------------------------------------
1 | import DropdownInitDirective from './dropdown-init.directive';
2 |
3 | describe('DropdownInit Directive', () => {
4 | let scope;
5 |
6 | beforeEach(() => {
7 | angular.module('test', [])
8 | .directive('aioDropdownInit', DropdownInitDirective);
9 | angular.mock.module('test');
10 | });
11 |
12 | beforeEach(() => {
13 | angular.mock.inject(($rootScope, $compile) => {
14 | scope = $rootScope.$new();
15 | spyOn($.fn, 'dropdown').and.callThrough();
16 | $compile('')(scope);
17 | scope.$digest();
18 | });
19 | });
20 |
21 | it('should call dropdown function when initialization', () => {
22 | expect($.fn.dropdown).toHaveBeenCalled();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/focus-me.directive.js:
--------------------------------------------------------------------------------
1 | function FocusMeDirective () {
2 | return {
3 | restrict: 'A',
4 | link
5 | };
6 |
7 | function link (scope, element, attrs) {
8 | scope.$watch(attrs.aioFocusMe, (val) => {
9 | if (val) {
10 | scope.$evalAsync(() => {
11 | element[0].focus();
12 | });
13 | }
14 | });
15 | }
16 | }
17 |
18 | FocusMeDirective.$inject = [];
19 |
20 | export default FocusMeDirective;
21 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/focus-me.directive.spec.js:
--------------------------------------------------------------------------------
1 | import FocusMeDirective from './focus-me.directive';
2 |
3 | describe('FocusMe Directive', () => {
4 | let scope;
5 | let element;
6 |
7 | beforeEach(() => {
8 | angular.module('test', [])
9 | .directive('aioFocusMe', FocusMeDirective);
10 | angular.mock.module('test');
11 | });
12 |
13 | beforeEach(() => {
14 | angular.mock.inject(($rootScope, $compile) => {
15 | scope = $rootScope.$new();
16 | scope.isFocus = true;
17 | element = $compile('')(scope);
18 | // spy needs to be put before $digest
19 | spyOn(element[0], 'focus');
20 | });
21 | });
22 |
23 | it('should make element get focus when attribute is true', () => {
24 | scope.$digest();
25 | expect(element[0].focus).toHaveBeenCalled();
26 | });
27 |
28 | it('should make element lose focus when attribute is false', () => {
29 | scope.isFocus = false;
30 | scope.$digest();
31 | expect(element[0].focus).not.toHaveBeenCalled();
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/loading-button.directive.js:
--------------------------------------------------------------------------------
1 | function LoadingButtonDirective () {
2 | return {
3 | restrict: 'A',
4 | link
5 | };
6 |
7 | function link (scope, element, attrs) {
8 | const spinner = '';
9 | scope.$watch(attrs.aioLoadingButton, (val) => {
10 | if (val) {
11 | element.prepend(spinner);
12 | } else {
13 | // jqLite only support find by tag name
14 | element.find('i').remove();
15 | }
16 | });
17 | }
18 | }
19 |
20 | LoadingButtonDirective.$inject = [];
21 |
22 | export default LoadingButtonDirective;
23 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/loading-button.directive.spec.js:
--------------------------------------------------------------------------------
1 | import LoadingButtonDirective from './loading-button.directive';
2 |
3 | describe('LoadingButton Directive', () => {
4 | let scope;
5 | let element;
6 |
7 | beforeEach(() => {
8 | angular.module('test', [])
9 | .directive('aioLoadingButton', LoadingButtonDirective);
10 | angular.mock.module('test');
11 | });
12 |
13 | beforeEach(() => {
14 | angular.mock.inject(($rootScope, $compile) => {
15 | scope = $rootScope.$new();
16 | scope.isLoading = false;
17 | element = $compile('')(scope);
18 | });
19 | });
20 |
21 | it('should not have loading spinner with false attribute', () => {
22 | scope.$digest();
23 | expect(element.find('i').length).toEqual(0);
24 | });
25 |
26 | it('should have loading spinner when attribute changes to true', () => {
27 | scope.isLoading = true;
28 | scope.$digest();
29 | expect(element.find('i').length).toEqual(1);
30 | // change back to false and spinner will be removed
31 | scope.isLoading = false;
32 | scope.$digest();
33 | expect(element.find('i').length).toEqual(0);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/select-init.directive.js:
--------------------------------------------------------------------------------
1 | function SelectInitDirective () {
2 | return {
3 | restrict: 'A',
4 | link
5 | };
6 |
7 | function link (scope, element, attrs) {
8 | scope.$watch(attrs.ngModel, initSelect);
9 | function initSelect () {
10 | element.siblings('.caret').remove();
11 | element.material_select();
12 | }
13 | }
14 | }
15 |
16 | SelectInitDirective.$inject = [];
17 |
18 | export default SelectInitDirective;
19 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/select-init.directive.spec.js:
--------------------------------------------------------------------------------
1 | import SelectInitDirective from './select-init.directive';
2 |
3 | describe('SelectInit Directive', () => {
4 | let scope;
5 |
6 | beforeEach(() => {
7 | angular.module('test', [])
8 | .directive('aioSelectInit', SelectInitDirective);
9 | angular.mock.module('test');
10 | });
11 |
12 | beforeEach(() => {
13 | angular.mock.inject(($rootScope, $compile) => {
14 | scope = $rootScope.$new();
15 | spyOn($.fn, 'material_select').and.callThrough();
16 | scope.fakeArr = ['a', 'b', 'c'];
17 | $compile('')(scope);
18 | scope.$digest();
19 | });
20 | });
21 |
22 | it('should call select function when initialization', () => {
23 | expect($.fn.material_select).toHaveBeenCalled();
24 | });
25 |
26 | it('should call select when model changes', () => {
27 | scope.fakeArr = ['a'];
28 | scope.$digest();
29 | expect($.fn.material_select.calls.count()).toEqual(2);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/tooltip-init.directive.js:
--------------------------------------------------------------------------------
1 | function TooltipInitDirective () {
2 | return {
3 | restrict: 'A',
4 | link
5 | };
6 |
7 | function link (scope, element) {
8 | element.tooltip();
9 | // destroy tooltip, otherwise it will still display
10 | scope.$on('$destroy', () => {
11 | element.tooltip('remove');
12 | });
13 | }
14 | }
15 |
16 | TooltipInitDirective.$inject = [];
17 |
18 | export default TooltipInitDirective;
19 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/tooltip-init.directive.spec.js:
--------------------------------------------------------------------------------
1 | import TooltipInitDirective from './tooltip-init.directive';
2 |
3 | describe('TooltipInit Directive', () => {
4 | let scope;
5 |
6 | beforeEach(() => {
7 | angular.module('test', [])
8 | .directive('aioTooltipInit', TooltipInitDirective);
9 | angular.mock.module('test');
10 | });
11 |
12 | beforeEach(() => {
13 | angular.mock.inject(($rootScope, $compile) => {
14 | scope = $rootScope.$new();
15 | spyOn($.fn, 'tooltip').and.callThrough();
16 | $compile('')(scope);
17 | scope.$digest();
18 | });
19 | });
20 |
21 | it('should call tooltip function when initialization', () => {
22 | expect($.fn.tooltip).toHaveBeenCalled();
23 | });
24 |
25 | it('should remove tooltip when destory', () => {
26 | expect($.fn.tooltip.calls.count()).toEqual(1);
27 | scope.$destroy();
28 | expect($.fn.tooltip).toHaveBeenCalledWith('remove');
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/validate-number.directive.js:
--------------------------------------------------------------------------------
1 | function ValidateNumberDirective () {
2 | return {
3 | require: 'ngModel',
4 | restrict: 'A',
5 | link
6 | };
7 |
8 | function link (scope, element, attrs, ctrl) {
9 | const pattern = /^\d+(\.\d{1,2})?$/;
10 | ctrl.$validators.number = function numberValidator (modelValue, viewModel) {
11 | if (pattern.test(viewModel)) {
12 | return true;
13 | }
14 | return false;
15 | };
16 | }
17 | }
18 |
19 | ValidateNumberDirective.$inject = [];
20 |
21 | export default ValidateNumberDirective;
22 |
--------------------------------------------------------------------------------
/source/app/components/_common/directives/validate-number.directive.spec.js:
--------------------------------------------------------------------------------
1 | import ValidateNumberDirective from './validate-number.directive';
2 |
3 | describe('ValidateNumber Directive', () => {
4 | let scope;
5 | let form;
6 |
7 | beforeEach(() => {
8 | angular.module('test', [])
9 | .directive('aioValidateNumber', ValidateNumberDirective);
10 | angular.mock.module('test');
11 | });
12 |
13 | beforeEach(() => {
14 | angular.mock.inject(($rootScope, $compile) => {
15 | scope = $rootScope.$new();
16 | scope.model = null;
17 | $compile(
18 | `
`
21 | )(scope);
22 | scope.$digest();
23 | form = scope.form;
24 | });
25 | });
26 |
27 | it('should make element valid with number input', () => {
28 | // input simulation
29 | form.numberInput.$setViewValue('123');
30 | scope.$digest();
31 | expect(scope.model).toEqual('123');
32 | expect(form.numberInput.$valid).toBe(true);
33 | expect(form.numberInput.$error.number).not.toBeDefined();
34 | });
35 |
36 | it('should make element invalid with non number input', () => {
37 | form.numberInput.$setViewValue('abc');
38 | scope.$digest();
39 | expect(scope.model).not.toBeDefined();
40 | expect(form.numberInput.$valid).toBe(false);
41 | expect(form.numberInput.$error.number).toBe(true);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/source/app/components/_common/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | // services
4 | import EventConstant from './services/event.constant';
5 | import AjaxErrorHandlerService from './services/ajax-error-handler.service';
6 | import ErrorService from './services/error.service';
7 | import LoggerService from './services/logger.service';
8 | import ResolveService from './services/resolve.service';
9 | import RouterHelperProvider from './services/router-helper.provider';
10 | import UserService from './services/user.service';
11 | // directives
12 | import FocusMeDirective from './directives/focus-me.directive';
13 | import LoadingButtonDirective from './directives/loading-button.directive';
14 | import ValidateNumberDirective from './directives/validate-number.directive';
15 | import DropdownInitDirective from './directives/dropdown-init.directive';
16 | import TooltipInitDirective from './directives/tooltip-init.directive';
17 | import SelectInitDirective from './directives/select-init.directive';
18 | import DatepickerInitDirective from './directives/datepicker-init.directive';
19 | // config
20 | import appConfig from './config';
21 | // run
22 | import appRun from './run';
23 | // production
24 | import {appProductionConfig, exceptionHandlerConfig} from './production/production.config';
25 |
26 | const common = angular.module('app.common', []);
27 |
28 | common
29 | .constant('Event', EventConstant)
30 | .service('AjaxErrorHandler', AjaxErrorHandlerService)
31 | .service('Error', ErrorService)
32 | .service('Logger', LoggerService)
33 | .service('Resolve', ResolveService)
34 | .provider('RouterHelper', RouterHelperProvider)
35 | .service('UserAPI', UserService)
36 | .directive('aioFocusMe', FocusMeDirective)
37 | .directive('aioLoadingButton', LoadingButtonDirective)
38 | .directive('aioValidateNumber', ValidateNumberDirective)
39 | .directive('aioDropdownInit', DropdownInitDirective)
40 | .directive('aioTooltipInit', TooltipInitDirective)
41 | .directive('aioSelectInit', SelectInitDirective)
42 | .directive('aioDatepickerInit', DatepickerInitDirective)
43 | .config(appConfig);
44 |
45 | if (__PROD__) { // eslint-disable-line no-undef
46 | common
47 | .config(exceptionHandlerConfig)
48 | .config(appProductionConfig);
49 | }
50 |
51 | common.run(appRun);
52 |
53 | export default common;
54 |
--------------------------------------------------------------------------------
/source/app/components/_common/production/exception-handler.decorator.js:
--------------------------------------------------------------------------------
1 | // Extend the original $exceptionHandler service.
2 | // * add error log prefix using exceptionPrefixProvider
3 | // * do other thing with error log
4 | exceptionHandlerDecorator.$inject = ['$delegate', 'Logger'];
5 | function exceptionHandlerDecorator ($delegate, logger) {
6 | return (exception, cause) => { // eslint-disable-line
7 | const appErrorPrefix = '[Aio Angular App Error] ';
8 | const errorData = {exception, cause};
9 | exception.message = appErrorPrefix + exception.message;
10 | // $delegate(exception, cause);
11 | /**
12 | * Could add the error to a service's collection,
13 | * add errors to $rootScope, log errors to remote web server,
14 | * or log locally. Or throw hard. It is entirely up to you.
15 | * throw exception;
16 | *
17 | * @example
18 | * throw { message: 'error message we added' };
19 | */
20 | logger.error(exception.message, errorData);
21 | };
22 | }
23 |
24 | export default exceptionHandlerDecorator;
25 |
--------------------------------------------------------------------------------
/source/app/components/_common/production/exception-handler.decorator.spec.js:
--------------------------------------------------------------------------------
1 | import exceptionHandlerDecorator from './exception-handler.decorator';
2 |
3 | describe('ExceptionHandler Decorator', () => {
4 | let Logger;
5 | let $timeout;
6 |
7 | // module defination, module load
8 | beforeEach(() => {
9 | angular.module('test', [])
10 | .config(($provide) => {
11 | $provide.decorator('$exceptionHandler', exceptionHandlerDecorator);
12 | });
13 | angular.mock.module('test');
14 | });
15 |
16 | // mock dependences
17 | beforeEach(() => {
18 | angular.mock.module(($provide) => {
19 | $provide.value('Logger', jasmine.createSpyObj('Logger', ['error']));
20 | });
21 | });
22 |
23 | // inject dependences
24 | beforeEach(() => {
25 | angular.mock.inject((_Logger_, _$timeout_) => {
26 | Logger = _Logger_;
27 | $timeout = _$timeout_;
28 | });
29 | });
30 |
31 | it('should call Logger.error when error happens', () => {
32 | const prefix = '[Aio Angular App Error]';
33 | // try to throw a error
34 | const error = new Error('exception');
35 | $timeout(() => {
36 | throw error;
37 | });
38 | $timeout.flush();
39 | expect(Logger.error).toHaveBeenCalledWith(`${prefix} exception`, {
40 | exception: error,
41 | cause: undefined // eslint-disable-line
42 | });
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/source/app/components/_common/production/production.config.js:
--------------------------------------------------------------------------------
1 | import exceptionHandlerDecorator from './exception-handler.decorator';
2 |
3 | appProductionConfig.$inject = ['$logProvider', '$compileProvider'];
4 | function appProductionConfig ($logProvider, $compileProvider) {
5 | $logProvider.debugEnabled(false);
6 | $compileProvider.debugInfoEnabled(false);
7 | }
8 |
9 | exceptionHandlerConfig.$inject = ['$provide'];
10 | function exceptionHandlerConfig ($provide) {
11 | // Use decorator to extend the original $exceptionHandler:
12 | $provide.decorator('$exceptionHandler', exceptionHandlerDecorator);
13 | }
14 |
15 | export {appProductionConfig, exceptionHandlerConfig};
16 |
--------------------------------------------------------------------------------
/source/app/components/_common/run.js:
--------------------------------------------------------------------------------
1 | appRun.$inject = ['$rootScope'];
2 | function appRun ($rootScope) {
3 | // TODO: need to figure out a btter way
4 | // IE hack
5 | $rootScope.ieClass = /MSIE|Trident/.test(window.navigator.userAgent) ? 'ie' : 'no-ie';
6 | }
7 |
8 | export default appRun;
9 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/ajax-error-handler.service.js:
--------------------------------------------------------------------------------
1 | class AjaxErrorHandlerService {
2 | constructor (Error, $q) {
3 | Object.assign(this, {Error, $q});
4 | }
5 | // directly reject with the human readable error message
6 | catcher (reason) {
7 | // reason is:
8 | // 1. either an error $http response
9 | // 2. or an error message returned by _success
10 | const type = typeof reason;
11 | let code = '$UNEXPECTED';
12 | if (reason) {
13 | if (type === 'object') {
14 | code = reason.message;
15 | } else if (type === 'string') {
16 | code = reason;
17 | }
18 | }
19 | return this.$q.reject({
20 | code,
21 | text: this.Error.getErrorMessage(code)
22 | });
23 | }
24 | }
25 |
26 | AjaxErrorHandlerService.$inject = ['Error', '$q'];
27 |
28 | export default AjaxErrorHandlerService;
29 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/ajax-error-handler.service.spec.js:
--------------------------------------------------------------------------------
1 | import AjaxErrorHandlerService from './ajax-error-handler.service';
2 |
3 | // for service, we can directly unit test the class without Angular
4 | describe('AjaxErrorHandler Service', () => {
5 | let ErrorService;
6 | let $q;
7 | let ajaxErrorHandler;
8 |
9 | beforeEach(() => {
10 | ErrorService = jasmine.createSpyObj('Error', ['getErrorMessage']);
11 | $q = jasmine.createSpyObj('$q', ['reject']);
12 | ajaxErrorHandler = new AjaxErrorHandlerService(ErrorService, $q);
13 | });
14 |
15 | describe('constructor function', () => {
16 | it('should set passed parameters correctly by constructor', () => {
17 | expect(ajaxErrorHandler.Error).toBe(ErrorService);
18 | expect(ajaxErrorHandler.$q).toBe($q);
19 | });
20 | });
21 |
22 | describe('catcher function', () => {
23 | function expectCommon (input, errorCode) {
24 | ajaxErrorHandler.catcher(input);
25 | expect(ErrorService.getErrorMessage).toHaveBeenCalledWith(errorCode);
26 | expect($q.reject).toHaveBeenCalledWith({
27 | code: errorCode,
28 | text: ErrorService.getErrorMessage(errorCode)
29 | });
30 | }
31 |
32 | it('should get correct code/message with string input', () => {
33 | const input = 'test';
34 | expectCommon(input, input);
35 | });
36 |
37 | it('should get correct code/message with object input', () => {
38 | const input = {
39 | message: 'test'
40 | };
41 | expectCommon(input, input.message);
42 | });
43 |
44 | it('should get correct code/message with null input', () => {
45 | const defaultString = '$UNEXPECTED';
46 | expectCommon(null, defaultString);
47 | });
48 |
49 | it('should get correct code/message with number input', () => {
50 | const defaultString = '$UNEXPECTED';
51 | expectCommon(123, defaultString);
52 | });
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/error.service.js:
--------------------------------------------------------------------------------
1 | // all error messages
2 | const ERROR_MESSAGE = {
3 | // unexpected
4 | $UNEXPECTED: 'Server issue, please try later!',
5 | // login
6 | LOGIN_WRONG_EMAIL_PASSWORD_PAIR: 'Incorrect email or password, please try again!',
7 | LOGIN_USER_IN_LOCK: 'Your account is locked!',
8 | // phone
9 | PHONE_QUERY_NOT_FOUND: 'Sorry, the phone you queryed can not be found!',
10 | PHONE_UPDATE_NOT_FOUND: 'Sorry, the phone you updated can not be found!',
11 | PHONE_DELETE_NOT_FOUND: 'Sorry, the phone you deleted can not be found!'
12 | };
13 |
14 | class ErrorService {
15 | getErrorMessage (errorCode) {
16 | return ERROR_MESSAGE[errorCode] || ERROR_MESSAGE.$UNEXPECTED;
17 | }
18 | }
19 |
20 | export default ErrorService;
21 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/error.service.spec.js:
--------------------------------------------------------------------------------
1 | import ErrorService from './error.service';
2 |
3 | // for service, we can directly unit test the class without Angular
4 | describe('Error Service', () => {
5 | let error;
6 |
7 | beforeEach(() => {
8 | error = new ErrorService();
9 | });
10 |
11 | describe('getErrorMessage function', () => {
12 | it('should get correct error message with a known error', () => {
13 | const errorCode = 'LOGIN_USER_IN_LOCK';
14 | const errorText = 'Your account is locked!';
15 | expect(error.getErrorMessage(errorCode)).toEqual(errorText);
16 | });
17 |
18 | it('should get correct error message with unknown error', () => {
19 | const errorCode = 'aaaa';
20 | const errorText = 'Server issue, please try later!';
21 | expect(error.getErrorMessage(errorCode)).toEqual(errorText);
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/event.constant.js:
--------------------------------------------------------------------------------
1 | // all events
2 | const EVENT = {
3 | AUTH_LOGIN: 'auth_login_event',
4 | AUTH_LOGOUT: 'auth_logout_event',
5 | AUTH_SESSION_VALID: 'auth_session_valid_event'
6 | };
7 |
8 | export default EVENT;
9 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/event.constant.spec.js:
--------------------------------------------------------------------------------
1 | import EventConstant from './event.constant';
2 |
3 | // for service, we can directly unit test the class without Angular
4 | describe('Event Constant', () => {
5 | it('should get correct event string with a known event', () => {
6 | expect(EventConstant.AUTH_LOGIN).toBe('auth_login_event');
7 | });
8 |
9 | it('should get undefined with unknown event', () => {
10 | expect(EventConstant.aaa).not.toBeDefined();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/logger.service.js:
--------------------------------------------------------------------------------
1 | // A wrapper service for original $log
2 | class LoggerService {
3 | constructor ($log) {
4 | Object.assign(this, {$log});
5 | }
6 |
7 | error (message, data) {
8 | this.$log.error(`Error: ${message}, ${data}`);
9 | }
10 |
11 | info (message, data) {
12 | this.$log.info(`Info: ${message}, ${data}`);
13 | }
14 |
15 | debug (message, data) {
16 | this.$log.debug(`Debug: ${message}, ${data}`);
17 | }
18 |
19 | warning (message, data) {
20 | this.$log.warn(`Warning: ${message}, ${data}`);
21 | }
22 | }
23 |
24 | LoggerService.$inject = ['$log'];
25 |
26 | export default LoggerService;
27 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/logger.service.spec.js:
--------------------------------------------------------------------------------
1 | import LoggerService from './logger.service';
2 |
3 | // for service, we can directly unit test the class without Angular
4 | describe('Logger Service', () => {
5 | let $log;
6 | let logger;
7 | const message = 'message';
8 | const data = 'data';
9 |
10 | beforeEach(() => {
11 | $log = jasmine.createSpyObj('$log', ['error', 'info', 'debug', 'warn']);
12 | logger = new LoggerService($log);
13 | });
14 |
15 | describe('constructor function', () => {
16 | it('should set passed parameters correctly by constructor', () => {
17 | expect(logger.$log).toBe($log);
18 | });
19 | });
20 |
21 | function expectCommon (func, prefix) {
22 | expect($log[func]).toHaveBeenCalledWith(`${prefix}: ${message}, ${data}`);
23 | }
24 |
25 | describe('error function', () => {
26 | it('should call $log.error with correct parameters', () => {
27 | logger.error(message, data);
28 | expectCommon('error', 'Error');
29 | });
30 | });
31 |
32 | describe('info function', () => {
33 | it('should call $log.info with correct parameters', () => {
34 | logger.info(message, data);
35 | expectCommon('info', 'Info');
36 | });
37 | });
38 |
39 | describe('debug function', () => {
40 | it('should call $log.debug with correct parameters', () => {
41 | logger.debug(message, data);
42 | expectCommon('debug', 'Debug');
43 | });
44 | });
45 |
46 | describe('warning function', () => {
47 | it('should call $log.warning with correct parameters', () => {
48 | logger.warning(message, data);
49 | expectCommon('warn', 'Warning');
50 | });
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/resolve.service.js:
--------------------------------------------------------------------------------
1 | class ResolveService {
2 | login (UserAPI, $q) {
3 | if (UserAPI.isLoggedIn() !== true) {
4 | return UserAPI.checkLoggedInStatus()
5 | .catch(_error);
6 | }
7 |
8 | function _error () {
9 | return $q.reject('requireLogin');
10 | }
11 | }
12 | }
13 |
14 | ResolveService.prototype.login.$inject = ['UserAPI', '$q'];
15 |
16 | export default ResolveService;
17 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/resolve.service.spec.js:
--------------------------------------------------------------------------------
1 | import ResolveService from './resolve.service';
2 |
3 | // for service, we can directly unit test the class without Angular
4 | describe('Resolve Service', () => {
5 | let UserService;
6 | let $q;
7 | let Resolve;
8 | let $rootScope;
9 |
10 | beforeEach(() => {
11 | angular.module('test', [])
12 | .service('Resolve', ResolveService);
13 | angular.mock.module('test');
14 | });
15 |
16 | beforeEach(() => {
17 | angular.mock.inject((_$q_, _$rootScope_, _Resolve_) => {
18 | $q = _$q_;
19 | $rootScope = _$rootScope_;
20 | Resolve = _Resolve_;
21 | UserService = jasmine.createSpyObj('UserService', ['isLoggedIn', 'checkLoggedInStatus']);
22 | spyOn($q, 'reject').and.callThrough();
23 | });
24 | });
25 |
26 | describe('login function', () => {
27 | describe('user has already logged in', () => {
28 | beforeEach(() => {
29 | UserService.isLoggedIn.and.returnValue(true);
30 | Resolve.login(UserService, $q);
31 | });
32 |
33 | it('should not reject', () => {
34 | expect($q.reject).not.toHaveBeenCalled();
35 | });
36 | });
37 |
38 | describe('user has not logged in', () => {
39 | let deferred;
40 | beforeEach(() => {
41 | deferred = $q.defer();
42 | UserService.isLoggedIn.and.returnValue(false);
43 | UserService.checkLoggedInStatus.and.returnValue(deferred.promise);
44 | Resolve.login(UserService, $q);
45 | });
46 | it('should not reject if UserService\'s checkLoggedInStatus resolves', () => {
47 | deferred.resolve();
48 | $rootScope.$digest();
49 | expect($q.reject).not.toHaveBeenCalled();
50 | });
51 |
52 | it('should reject if UserService\'s checkLoggedInStatus rejects', () => {
53 | deferred.reject();
54 | $rootScope.$digest();
55 | expect($q.reject).toHaveBeenCalledWith('requireLogin');
56 | });
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/router-helper.provider.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | const [handlingStateChangeError, hasOtherwise, stateCounts] = [
4 | Symbol(), Symbol(), Symbol()
5 | ];
6 |
7 | class RouterHelper {
8 | constructor (config, $stateProvider, $urlRouterProvider,
9 | $rootScope, $state, Logger, Resolve) {
10 | Object.assign(this, {config, $stateProvider, $urlRouterProvider,
11 | $rootScope, $state, Logger, Resolve});
12 | // private variable
13 | this[handlingStateChangeError] = false;
14 | this[hasOtherwise] = false;
15 | this[stateCounts] = {
16 | errors: 0,
17 | changes: 0
18 | };
19 |
20 | this.handleRoutingErrors();
21 | this.updateDocTitle();
22 | }
23 |
24 | configureStates (states, otherwisePath) {
25 | const self = this;
26 | states.forEach((state) => {
27 | // add login check if requireLogin is true
28 | const data = state.config.data;
29 | if (data && data.requireLogin === true) {
30 | state.config.resolve = angular.extend(
31 | state.config.resolve || {},
32 | {loginResolve: self.Resolve.login}
33 | );
34 | }
35 | state.config.resolve =
36 | angular.extend(state.config.resolve || {}, self.config.resolveAlways);
37 | this.$stateProvider.state(state.state, state.config);
38 | });
39 | if (otherwisePath && !this[hasOtherwise]) {
40 | this[hasOtherwise] = true;
41 | this.$urlRouterProvider.otherwise(otherwisePath);
42 | }
43 | }
44 |
45 | handleRoutingErrors () {
46 | // Route cancellation:
47 | // On routing error, go to the dashboard.
48 | // Provide an exit clause if it tries to do it twice.
49 | this.$rootScope.$on('$stateChangeError',
50 | (event, toState, toParams, fromState, fromParams, error) => {
51 | if (this[handlingStateChangeError]) {
52 | return;
53 | }
54 | this[stateCounts].errors++;
55 | this[handlingStateChangeError] = true;
56 | const destination = (toState &&
57 | (toState.title || toState.name || toState.loadedTemplateUrl)) ||
58 | 'unknown target';
59 | const errorMessage = (error && error.message) || error;
60 | const msg = `Error routing to ${destination}.\nReason: ${errorMessage}.`;
61 | this.Logger.warning(msg);
62 | // handle requireLogin issue
63 | if (error === 'requireLogin') {
64 | this.$state.prev = {
65 | state: toState.name,
66 | params: toParams
67 | };
68 | this.$state.go('root.layout.login');
69 | } else {
70 | this.$state.go('root.layout.home');
71 | }
72 | }
73 | );
74 | }
75 |
76 | getStates () {
77 | return this.$state.get();
78 | }
79 |
80 | updateDocTitle () {
81 | this.$rootScope.$on('$stateChangeSuccess',
82 | (event, toState) => {
83 | this[stateCounts].changes++;
84 | this[handlingStateChangeError] = false;
85 | const title = `${toState.data.title} - ${this.config.mainTitle}`;
86 | this.$rootScope.title = title; // data bind to
87 | this.$rootScope._class = toState.data._class; // data bind to
88 | }
89 | );
90 | }
91 | }
92 |
93 | // Help configure the state-base ui.router
94 | class RouterHelperProvider {
95 | constructor ($locationProvider, $stateProvider, $urlRouterProvider) {
96 | Object.assign(this, {$locationProvider, $stateProvider, $urlRouterProvider});
97 |
98 | this.config = {
99 | mainTitle: '',
100 | resolveAlways: {}
101 | };
102 | this.$locationProvider.html5Mode(true);
103 | }
104 |
105 | configure (cfg) {
106 | angular.extend(this.config, cfg);
107 | }
108 |
109 | $get ($rootScope, $state, Logger, Resolve) {
110 | return new RouterHelper(
111 | this.config, this.$stateProvider, this.$urlRouterProvider,
112 | $rootScope, $state, Logger, Resolve);
113 | }
114 | }
115 |
116 | RouterHelperProvider.prototype.$get.$inject = [
117 | '$rootScope', '$state', 'Logger', 'Resolve'
118 | ];
119 |
120 | RouterHelperProvider.$inject = ['$locationProvider', '$stateProvider', '$urlRouterProvider'];
121 |
122 | export default RouterHelperProvider;
123 |
--------------------------------------------------------------------------------
/source/app/components/_common/services/user.service.js:
--------------------------------------------------------------------------------
1 | const [isLoggedIn, userInfo] = [Symbol(), Symbol()];
2 | class UserSerivce {
3 | constructor ($http, $q, $rootScope, Event, AjaxError) {
4 | Object.assign(this, {$http, $q, $rootScope, Event, AjaxError});
5 | // private variable
6 | this[isLoggedIn] = false;
7 | this[userInfo] = null;
8 | }
9 |
10 | isLoggedIn () {
11 | return this[isLoggedIn];
12 | }
13 |
14 | checkLoggedInStatus () {
15 | const self = this;
16 | return this.$http.get('api/user/loginstatus')
17 | .then(_success)
18 | .catch(_error);
19 |
20 | function _success (response) {
21 | const data = response.data;
22 | if (response.status === 200 && data.code === 0) {
23 | self._setUser(data.result.user);
24 | self.$rootScope.$broadcast(self.Event.AUTH_SESSION_VALID, data.result.user);
25 | return data.result.user;
26 | }
27 | return self.$q.reject(data.message);
28 | }
29 |
30 | function _error (reason) {
31 | self._clearUser();
32 | return self.AjaxError.catcher(reason);
33 | }
34 | }
35 |
36 | login (email, password) {
37 | const self = this;
38 | const req = {
39 | email,
40 | password
41 | };
42 | return this.$http.post('api/user/login', req)
43 | .then(_success)
44 | .catch(_error);
45 |
46 | function _success (response) {
47 | const data = response.data;
48 | if (response.status === 200 && data.code === 0) {
49 | self._setUser(data.result.user);
50 | self.$rootScope.$broadcast(self.Event.AUTH_LOGIN, data.result.user);
51 | return data.result.user;
52 | }
53 | return self.$q.reject(data.message);
54 | }
55 |
56 | function _error (reason) {
57 | self._clearUser();
58 | return self.AjaxError.catcher(reason);
59 | }
60 | }
61 |
62 | logout () {
63 | const self = this;
64 | return this.$http.post('api/user/logout')
65 | .then(_success)
66 | .catch(_error);
67 |
68 | function _success (response) {
69 | const data = response.data;
70 | if (response.status === 200 && data.code === 0) {
71 | self._clearUser();
72 | self.$rootScope.$broadcast(self.Event.AUTH_LOGOUT);
73 | } else {
74 | return self.$q.reject(data.message);
75 | }
76 | }
77 |
78 | function _error (reason) {
79 | // log out user even API is failed
80 | self._clearUser();
81 | return self.AjaxError.catcher(reason);
82 | }
83 | }
84 |
85 | getUserInfo () {
86 | return this[userInfo];
87 | }
88 |
89 | getProductSummary () {
90 | const self = this;
91 | return this.$http.get('api/user/products')
92 | .then(_success)
93 | .catch(this.AjaxError.catcher.bind(this.AjaxError));
94 |
95 | function _success (response) {
96 | const data = response.data;
97 | if (response.status === 200 && data.code === 0) {
98 | return data.result.summary;
99 | }
100 | return self.$q.reject(data.message);
101 | }
102 | }
103 |
104 | _setUser (userData) {
105 | this[isLoggedIn] = true;
106 | this[userInfo] = userData;
107 | }
108 |
109 | _clearUser () {
110 | this[isLoggedIn] = false;
111 | this[userInfo] = null;
112 | }
113 | }
114 |
115 | UserSerivce.$inject = ['$http', '$q', '$rootScope', 'Event', 'AjaxErrorHandler'];
116 |
117 | export default UserSerivce;
118 |
--------------------------------------------------------------------------------
/source/app/components/_common/styles/animation.styl:
--------------------------------------------------------------------------------
1 | .dissolve-animation
2 | &.ng-hide-remove, &.ng-hide-add
3 | transition 0.5s linear all
4 | &.ng-hide-remove.ng-hide-remove-active, &.ng-hide-add
5 | opacity 1
6 | &.ng-hide-add.ng-hide-add-active, &.ng-hide-remove
7 | opacity 0
8 |
9 | .icon-breath-animation
10 | animation icon-breath 1.5s ease-in-out infinite
11 |
12 | .icon-rotate-animation
13 | animation icon-rotate .8s linear infinite
14 | margin-right 5px
15 |
16 | @keyframes icon-breath
17 | from
18 | box-shadow 0 0 2px 5px rgba(255, 138, 138, 0.48)
19 | opacity 0.5
20 | to
21 | box-shadow none
22 | opacity 1
23 |
24 | @keyframes icon-rotate
25 | from
26 | transform rotate(0deg)
27 | to
28 | transform rotate(360deg)
29 |
--------------------------------------------------------------------------------
/source/app/components/_common/styles/common.styl:
--------------------------------------------------------------------------------
1 | @import 'variable'
2 | @import 'mixin'
3 | @import 'responsive'
4 |
--------------------------------------------------------------------------------
/source/app/components/_common/styles/fonts.styl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PinkyJie/angular1-webpack-starter/6e7b0f6fd04a94df996dfeecc447f38b167a5196/source/app/components/_common/styles/fonts.styl
--------------------------------------------------------------------------------
/source/app/components/_common/styles/helper.styl:
--------------------------------------------------------------------------------
1 | @import './common'
2 |
3 | // handy class for show/hide in different screens
4 | .hide-sm
5 | +sm()
6 | display none
7 |
8 | .hide-gt-sm
9 | +generate-media($sizes[0])
10 | display none
11 |
12 | .hide-md
13 | +md()
14 | display none
15 |
16 | .hide-gt-md
17 | +generate-media($sizes[1])
18 | display none
19 |
20 | .hide-lg
21 | +lg()
22 | display none
23 |
24 | .hide-gt-lg
25 | +generate-media($sizes[2])
26 | display none
27 |
28 | // handy class for margin/padding
29 | $marginPaddingStep = 8
30 | $marginPaddingMap = {
31 | 'm': 'margin',
32 | 'p': 'padding'
33 | }
34 | $directionMap = {
35 | '': '',
36 | 'l': 'left',
37 | 'r': 'right',
38 | 't': 'top',
39 | 'b': 'bottom'
40 | }
41 | // m1: margin 8px
42 | // ml2: margin-left 16px
43 | generate-margin-padding()
44 | for _prefix in $marginPaddingMap
45 | for _direction in $directionMap
46 | for _step in 1..5
47 | if _direction == ''
48 | _key = $marginPaddingMap[_prefix]
49 | else
50 | _key = $marginPaddingMap[_prefix] + '-' + $directionMap[_direction]
51 | _val = unit(_step * $marginPaddingStep, 'px')
52 | .{_prefix + _direction + _step}
53 | {_key}: _val
54 |
55 | generate-margin-padding()
56 |
--------------------------------------------------------------------------------
/source/app/components/_common/styles/mixin.styl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PinkyJie/angular1-webpack-starter/6e7b0f6fd04a94df996dfeecc447f38b167a5196/source/app/components/_common/styles/mixin.styl
--------------------------------------------------------------------------------
/source/app/components/_common/styles/responsive.styl:
--------------------------------------------------------------------------------
1 | // simple responsive utility
2 |
3 | // pass (min, max) will generate (min-width) and (max-width)
4 | // pass (min) will generate (min-width)
5 | generate-media(args...)
6 | _min_str = 'only screen and (min-width: %s)'
7 | _max_str = ' and (max-width: %s)'
8 | if length(args) == 1
9 | _condition = _min_str % unit(args[0], 'px')
10 | else
11 | _condition = (_min_str % unit(args[0], 'px')) + (_max_str % unit(args[1], 'px'))
12 | @media _condition
13 | {block}
14 |
15 | // small device: 0 ~ 600px
16 | sm()
17 | +generate-media(0, $sizes[0])
18 | {block}
19 |
20 | gt-sm()
21 | +generate-media($sizes[0])
22 | {block}
23 |
24 | // middle device: 600px ~ 960px
25 | md()
26 | +generate-media($sizes[0], $sizes[1])
27 | {block}
28 |
29 | // large device: 960px ~ 1200px
30 | lg()
31 | +generate-media($sizes[1], $sizes[2])
32 | {block}
33 |
34 | // extra large device: 1200px ~
35 | xl()
36 | +generate-media($sizes[2])
37 | {block}
38 |
--------------------------------------------------------------------------------
/source/app/components/_common/styles/variable.styl:
--------------------------------------------------------------------------------
1 | // screen sizes from https://material.angularjs.org
2 | $sizes = 600 960 1200
3 |
4 | // width/height
5 | $headerHeight = 64px
6 | $headerHeightForMobile = 56px
7 | $sidebarWidth = 200px
8 |
9 | // color
10 | $successColor = #4CAF50
11 | $errorColor = #F44336
12 | $warningColor = #FF9800
13 | $infoColor = #2196F3
14 |
15 | $loadingBarColor = #FF9E80
16 | $loadingBgColor = #FBE9E7
17 | $loadingBarShodowColor = #F1D9A4
18 |
--------------------------------------------------------------------------------
/source/app/components/_layout/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PinkyJie/angular1-webpack-starter/6e7b0f6fd04a94df996dfeecc447f38b167a5196/source/app/components/_layout/bg.png
--------------------------------------------------------------------------------
/source/app/components/_layout/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import common from '../_common';
4 | import appLayoutRun from './layout.route';
5 | import {header} from '../header';
6 | import {footer} from '../footer';
7 | import {sidebar} from '../sidebar';
8 | import {breadcrumb} from '../breadcrumb';
9 | import {sidebarSm} from '../sidebar-sm';
10 |
11 | export default angular.module('app.layout', [
12 | common.name,
13 | header.name,
14 | footer.name,
15 | sidebar.name,
16 | breadcrumb.name,
17 | sidebarSm.name
18 | ]).run(appLayoutRun);
19 |
--------------------------------------------------------------------------------
/source/app/components/_layout/layout.route.js:
--------------------------------------------------------------------------------
1 | import mainLayoutHtml from './main.layout.jade';
2 | import {headerHtml, HeaderController} from '../header';
3 | import {footerHtml, FooterController} from '../footer';
4 | import {sidebarHtml, SidebarController} from '../sidebar';
5 | import {breadcrumbHtml, BreadcrumbController} from '../breadcrumb';
6 | import {sidebarSmHtml, SidebarSmController} from '../sidebar-sm';
7 |
8 | appLayoutRun.$inject = ['RouterHelper'];
9 | function appLayoutRun (RouterHelper) {
10 | RouterHelper.configureStates(getStates());
11 | }
12 |
13 | function getStates () {
14 | return [
15 | {
16 | state: 'root',
17 | config: {
18 | abstract: true,
19 | url: '',
20 | template: mainLayoutHtml
21 | }
22 | },
23 | {
24 | state: 'root.layout',
25 | config: {
26 | abstract: true,
27 | url: '',
28 | views: {
29 | header: {
30 | template: headerHtml,
31 | controller: `${HeaderController.name} as vm`
32 | },
33 | sidebar: {
34 | template: sidebarHtml,
35 | controller: `${SidebarController.name} as vm`
36 | },
37 | breadcrumb: {
38 | template: breadcrumbHtml,
39 | controller: `${BreadcrumbController.name} as vm`
40 | },
41 | footer: {
42 | template: footerHtml,
43 | controller: `${FooterController.name} as vm`
44 | },
45 | 'sidebar-sm': {
46 | template: sidebarSmHtml,
47 | controller: `${SidebarSmController.name} as vm`
48 | }
49 | }
50 | }
51 | }
52 | ];
53 | }
54 |
55 | export default appLayoutRun;
56 |
--------------------------------------------------------------------------------
/source/app/components/_layout/layout.styl:
--------------------------------------------------------------------------------
1 | @import '../_common/styles/common'
2 |
3 | body
4 | .wrapper
5 | display flex
6 | flex-direction column
7 | background-image url('./bg.png')
8 | background-size cover
9 | .ie &
10 | height 100vh
11 | .no-ie &
12 | min-height 100vh
13 | .content
14 | margin-top $headerHeight
15 | +sm()
16 | margin-top $headerHeightForMobile
17 | display flex
18 | flex 1 1 auto
19 | .main-content
20 | flex 1 1 auto
21 | display flex
22 | flex-direction column
23 | overflow hidden
24 | .main-view
25 | flex 1 1 auto
26 | display flex
27 | justify-content center
28 | align-items center
29 | padding 0.5rem 0 1rem 0
30 | +sm()
31 | margin-left 8px
32 | margin-right 8px
33 | &.ng-enter
34 | animation bounceIn .8s
35 | .full-width
36 | align-self flex-start
37 | padding 10px 30px 20px 30px
38 | width 100%
39 |
40 | .dropdown-content
41 | width auto !important
42 |
43 | .select-dropdown
44 | > li.disabled > span
45 | color rgba(0, 0, 0, .3)
46 |
--------------------------------------------------------------------------------
/source/app/components/_layout/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PinkyJie/angular1-webpack-starter/6e7b0f6fd04a94df996dfeecc447f38b167a5196/source/app/components/_layout/logo.png
--------------------------------------------------------------------------------
/source/app/components/_layout/main.layout.jade:
--------------------------------------------------------------------------------
1 | - require('../_common/styles/animation.styl')
2 | - require('../_common/styles/fonts.styl')
3 | - require('../_common/styles/helper.styl')
4 | - require('./layout.styl')
5 | //- header/content/footer view
6 | header.header-view(ui-view="header")
7 | .content
8 | .sidebar-view(ui-view="sidebar", ng-class="{'show': showSidebar}")
9 | .main-content
10 | .breadcrumb-view(ui-view="breadcrumb")
11 | .main-view(ui-view="main")
12 | footer.footer-view(ui-view="footer")
13 | .sidebar-sm-view(ui-view="sidebar-sm")
14 |
--------------------------------------------------------------------------------
/source/app/components/banner/banner.directive.js:
--------------------------------------------------------------------------------
1 | import bannerHtml from './banner.jade';
2 |
3 | function BannerDirective () {
4 | return {
5 | restrict: 'AE',
6 | scope: {
7 | text: '='
8 | },
9 | template: bannerHtml
10 | };
11 | }
12 |
13 | BannerDirective.$inject = [];
14 |
15 | export default BannerDirective;
16 |
--------------------------------------------------------------------------------
/source/app/components/banner/banner.directive.spec.js:
--------------------------------------------------------------------------------
1 | import BannerDirective from './banner.directive';
2 |
3 | describe('Banner Directive', () => {
4 | let element;
5 |
6 | beforeEach(() => {
7 | angular.module('test', [])
8 | .directive('aioBanner', BannerDirective);
9 | angular.mock.module('test');
10 | });
11 |
12 | beforeEach(() => {
13 | angular.mock.inject(($compile, $rootScope) => {
14 | const scope = $rootScope.$new();
15 | scope.text = 'test text';
16 | element = $compile('')(scope);
17 | scope.$digest();
18 | });
19 | });
20 |
21 | it('should has correct content passed in', () => {
22 | expect(element.find('p').text()).toEqual('test text');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/source/app/components/banner/banner.jade:
--------------------------------------------------------------------------------
1 | .banner-view
2 | .card-panel.blue.lighten-4.p1
3 | p {{text}}
4 |
--------------------------------------------------------------------------------
/source/app/components/banner/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import BannerDirective from './banner.directive';
4 |
5 | const banner = angular.module('app.components.banner', [])
6 | .directive(`aioBanner`, BannerDirective);
7 |
8 | export default banner;
9 |
--------------------------------------------------------------------------------
/source/app/components/breadcrumb/breadcrumb.controller.js:
--------------------------------------------------------------------------------
1 | class BreadcrumbController {
2 | constructor ($state, $rootScope) {
3 | Object.assign(this, {$state, $rootScope});
4 |
5 | this._applyNewBreadcrumb(this.$state.current, this.$state.params);
6 | this.$rootScope.$on('$stateChangeSuccess',
7 | (event, toState, toParams) => {
8 | this._applyNewBreadcrumb(toState, toParams);
9 | });
10 | }
11 |
12 | _applyNewBreadcrumb (state, params) {
13 | this.breadcrumbs = [];
14 | const curName = state.name;
15 | const parentStateNames = this._getAncestorStates(curName);
16 | parentStateNames.forEach((name) => {
17 | const stateConfig = this.$state.get(name);
18 | if (stateConfig.abstract) {
19 | return;
20 | }
21 | const breadcrumb = {
22 | link: name,
23 | text: stateConfig.breadcrumb
24 | };
25 | this.breadcrumbs.push(breadcrumb);
26 | });
27 | const length = this.breadcrumbs.length;
28 | if (params && length > 0) {
29 | const lastBreadcrumb = this.breadcrumbs[length - 1];
30 | lastBreadcrumb.link = `${lastBreadcrumb.link}(${JSON.stringify(params)})`;
31 | }
32 | }
33 |
34 | _getAncestorStates (stateName) {
35 | const ancestors = [];
36 | const pieces = stateName.split('.');
37 | if (pieces.length > 1) {
38 | for (let i = 1; i < pieces.length; i++) {
39 | const name = pieces.slice(0, i + 1);
40 | ancestors.push(name.join('.'));
41 | }
42 | }
43 | return ancestors;
44 | }
45 | }
46 |
47 | BreadcrumbController.$inject = ['$state', '$rootScope'];
48 |
49 | export default BreadcrumbController;
50 |
--------------------------------------------------------------------------------
/source/app/components/breadcrumb/breadcrumb.controller.spec.js:
--------------------------------------------------------------------------------
1 | import BreadcrumbController from './breadcrumb.controller';
2 |
3 | describe('Breadcrumb Controller', () => {
4 | let controller;
5 | let $rootScope;
6 | let $state;
7 |
8 | beforeEach(() => {
9 | $rootScope = jasmine.createSpyObj('$rootScope', ['$on']);
10 | $state = jasmine.createSpyObj('$state', ['current', 'params', 'get']);
11 | controller = new BreadcrumbController($state, $rootScope);
12 | });
13 |
14 | describe('constructor function', () => {
15 | it('should init successfully', () => {
16 | expect(controller.$state).toBe($state);
17 | expect(controller.$rootScope).toBe($rootScope);
18 | expect($rootScope.$on).toHaveBeenCalled();
19 | expect($rootScope.$on.calls.argsFor(0)[0]).toEqual('$stateChangeSuccess');
20 | const callback = $rootScope.$on.calls.argsFor(0)[1];
21 | spyOn(controller, '_applyNewBreadcrumb');
22 | callback({}, 'a', 'b');
23 | expect(controller._applyNewBreadcrumb).toHaveBeenCalledWith('a', 'b');
24 | });
25 | });
26 |
27 | describe('_applyNewBreadcrumb function', () => {
28 | let currentState;
29 | beforeEach(() => {
30 | currentState = {
31 | name: 'a.b.c.d'
32 | };
33 | $state.get.and.callFake((name) => {
34 | let config;
35 | switch (name) {
36 | case 'a.b':
37 | config = {
38 | abstract: true
39 | };
40 | break;
41 | case 'a.b.c':
42 | config = {
43 | breadcrumb: 'ccc'
44 | };
45 | break;
46 | case 'a.b.c.d':
47 | config = {
48 | breadcrumb: 'ddd'
49 | };
50 | break;
51 | default:
52 | config = {};
53 | }
54 | return config;
55 | });
56 | });
57 |
58 | it('should generate correct breadcrumb list without params', () => {
59 | controller._applyNewBreadcrumb(currentState);
60 | const breadcrumbList = controller.breadcrumbs;
61 | expect(breadcrumbList[0]).toEqual({
62 | link: 'a.b.c',
63 | text: 'ccc'
64 | });
65 | expect(breadcrumbList[1]).toEqual({
66 | link: 'a.b.c.d',
67 | text: 'ddd'
68 | });
69 | });
70 |
71 | it('should generate correct breadcrumb list with params', () => {
72 | controller._applyNewBreadcrumb(currentState, {a: 1});
73 | const breadcrumbList = controller.breadcrumbs;
74 | expect(breadcrumbList[0]).toEqual({
75 | link: 'a.b.c',
76 | text: 'ccc'
77 | });
78 | expect(breadcrumbList[1]).toEqual({
79 | link: 'a.b.c.d({"a":1})',
80 | text: 'ddd'
81 | });
82 | });
83 | });
84 |
85 | describe('_getAncestorStates function', () => {
86 | it('should get the correct ancestor states', () => {
87 | expect(controller._getAncestorStates('a.b.c.d')).toEqual(['a.b', 'a.b.c', 'a.b.c.d']);
88 | expect(controller._getAncestorStates('a.b.c')).toEqual(['a.b', 'a.b.c']);
89 | expect(controller._getAncestorStates('a.b')).toEqual(['a.b']);
90 | expect(controller._getAncestorStates('a')).toEqual([]);
91 | });
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/source/app/components/breadcrumb/breadcrumb.jade:
--------------------------------------------------------------------------------
1 | - require('./breadcrumb.styl')
2 | ol.breadcrumb.p1
3 | li.home-item
4 | a.btn-floating.white.waves-effect.waves-light(ui-sref="root.layout.home")
5 | i.mdi-action-home.black-text
6 | li.breadcrumb-item(ng-repeat="nav in vm.breadcrumbs")
7 | i.mdi-navigation-chevron-right.small.white-text
8 | a.btn.white.black-text.waves-effect.waves-light(ng-if="!$last", ui-sref="{{nav.link}}")
9 | | {{nav.text}}
10 | span.white-text.nav-text(ng-if="$last") {{nav.text}}
11 |
--------------------------------------------------------------------------------
/source/app/components/breadcrumb/breadcrumb.styl:
--------------------------------------------------------------------------------
1 | .breadcrumb-view
2 | .breadcrumb
3 | margin 0
4 | list-style none
5 | li
6 | display inline-block
7 | .nav-text
8 | font-size 1.3rem
9 | display inline-block
10 | float right
11 | line-height 45px
12 | a
13 | margin-top -16px
14 | .breadcrumb-item
15 | margin-left -5px
16 |
--------------------------------------------------------------------------------
/source/app/components/breadcrumb/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import BreadcrumbController from './breadcrumb.controller';
4 | import breadcrumbHtml from './breadcrumb.jade';
5 | import common from '../_common';
6 |
7 | const breadcrumb = angular.module('app.components.breadcrumb', [common.name])
8 | .controller(BreadcrumbController.name, BreadcrumbController);
9 |
10 | export {breadcrumb, breadcrumbHtml, BreadcrumbController};
11 |
--------------------------------------------------------------------------------
/source/app/components/footer/footer.controller.js:
--------------------------------------------------------------------------------
1 | class FooterController {
2 | constructor () {
3 | this.year = (new Date()).getFullYear();
4 | }
5 | }
6 |
7 | FooterController.$inject = [];
8 |
9 | export default FooterController;
10 |
--------------------------------------------------------------------------------
/source/app/components/footer/footer.controller.spec.js:
--------------------------------------------------------------------------------
1 | import FooterController from './footer.controller';
2 |
3 | describe('Footer Controller', () => {
4 | let controller;
5 | beforeEach(() => {
6 | controller = new FooterController();
7 | });
8 |
9 | it('should have correct year', () => {
10 | expect(controller.year).toEqual((new Date()).getFullYear());
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/source/app/components/footer/footer.jade:
--------------------------------------------------------------------------------
1 | - require('./footer.styl')
2 | nav.blue-grey
3 | .nav-wrapper
4 | p.copyright.center-align
5 | a.white-text(href="https://github.com/PinkyJie/angular1-webpack-starter", target="_blank") Angular1 Webpack Starter
6 | | © {{::vm.year}}
7 |
--------------------------------------------------------------------------------
/source/app/components/footer/footer.styl:
--------------------------------------------------------------------------------
1 | .footer-view
2 | flex none
3 | nav
4 | .copyright
5 | margin auto
6 | text-shadow 1px 1px rgba(0, 0, 0, .2)
7 | a
8 | text-decoration underline
9 |
--------------------------------------------------------------------------------
/source/app/components/footer/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import FooterController from './footer.controller';
4 | import footerHtml from './footer.jade';
5 | import common from '../_common';
6 |
7 | const footer = angular.module('app.components.footer', [common.name])
8 | .controller(FooterController.name, FooterController);
9 |
10 | export {footer, footerHtml, FooterController};
11 |
--------------------------------------------------------------------------------
/source/app/components/header/header.controller.js:
--------------------------------------------------------------------------------
1 | class HeaderController {
2 | constructor ($rootScope, Event) {
3 | Object.assign(this, {$rootScope, Event});
4 |
5 | // update header based on auth event
6 | this.$rootScope.$on(this.Event.AUTH_LOGIN, this.updateHeader.bind(this));
7 | this.$rootScope.$on(this.Event.AUTH_LOGOUT, this.updateHeader.bind(this));
8 | this.$rootScope.$on(this.Event.AUTH_SESSION_VALID, this.updateHeader.bind(this));
9 | }
10 |
11 | updateHeader (e, userInfo) {
12 | if (userInfo) {
13 | this.isLoggedIn = true;
14 | this.userInfo = userInfo;
15 | } else {
16 | this.isLoggedIn = false;
17 | this.userInfo = null;
18 | }
19 | }
20 |
21 | switchSidebar () {
22 | this.$rootScope.showSidebar = !this.$rootScope.showSidebar;
23 | }
24 | }
25 |
26 | HeaderController.$inject = ['$rootScope', 'Event'];
27 |
28 | export default HeaderController;
29 |
--------------------------------------------------------------------------------
/source/app/components/header/header.controller.spec.js:
--------------------------------------------------------------------------------
1 | import HeaderController from './header.controller';
2 |
3 | describe('Header Controller', () => {
4 | let controller;
5 | let $rootScope;
6 | let EVENT;
7 |
8 | beforeEach(() => {
9 | $rootScope = jasmine.createSpyObj('$rootScope', ['$on']);
10 | EVENT = {
11 | AUTH_LOGIN: 1,
12 | AUTH_LOGOUT: 2,
13 | AUTH_SESSION_VALID: 3
14 | };
15 | controller = new HeaderController($rootScope, EVENT);
16 | });
17 |
18 | describe('constructor function', () => {
19 | it('should init successfully', () => {
20 | expect(controller.$rootScope).toBe($rootScope);
21 | expect(controller.Event).toBe(EVENT);
22 | expect($rootScope.$on.calls.count()).toEqual(3);
23 | expect($rootScope.$on.calls.argsFor(0)[0]).toEqual(1);
24 | expect($rootScope.$on.calls.argsFor(1)[0]).toEqual(2);
25 | expect($rootScope.$on.calls.argsFor(2)[0]).toEqual(3);
26 | });
27 | });
28 |
29 | describe('updateHeader function', () => {
30 | it('should set correct status with valid user info', () => {
31 | controller.updateHeader({}, 'user');
32 | expect(controller.isLoggedIn).toBe(true);
33 | expect(controller.userInfo).toEqual('user');
34 | });
35 |
36 | it('should set correct status with invalid user info', () => {
37 | controller.updateHeader({});
38 | expect(controller.isLoggedIn).toBe(false);
39 | expect(controller.userInfo).toEqual(null);
40 | });
41 | });
42 |
43 | describe('switchSidebar function', () => {
44 | it('should switch sidebar show/hide', () => {
45 | $rootScope.showSidebar = true;
46 | controller.switchSidebar();
47 | expect($rootScope.showSidebar).toBe(false);
48 | controller.switchSidebar();
49 | expect($rootScope.showSidebar).toBe(true);
50 | });
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/source/app/components/header/header.jade:
--------------------------------------------------------------------------------
1 | - require('./header.styl')
2 | - var logoPng = require('../_layout/logo.png')
3 | nav.blue
4 | .nav-wrapper
5 | img.logo(src= logoPng)
6 | a.brand-logo(ui-sref="root.layout.home") Aio Angular App
7 | ul.header-menus.right(ng-if="vm.isLoggedIn")
8 | li.header-user-name.hide-sm
9 | span {{vm.userInfo.name}}
10 | li.header-dropdown
11 | a.dropdown-button.waves-light.waves-effect(data-activates="header-dropdown-content", aio-dropdown-init)
12 | i.mdi-navigation-more-vert
13 | ul#header-dropdown-content.dropdown-content
14 | li.hide-gt-sm.center-align.dropdown-user-name
15 | span.grey-text {{vm.userInfo.name}}
16 | li.hide-gt-sm.divider
17 | li.logout-link
18 | a(ui-sref="root.layout.login({action: 'logout'})")
19 | i.mdi-action-exit-to-app.left
20 | | Logout
21 |
--------------------------------------------------------------------------------
/source/app/components/header/header.styl:
--------------------------------------------------------------------------------
1 | @import '../_common/styles/common'
2 |
3 | .header-view
4 | flex none
5 | position fixed
6 | width 100%
7 | z-index 999
8 | .nav-wrapper
9 | +sm()
10 | padding-left 0
11 | padding-right 0
12 | .logo
13 | width 44px
14 | height 44px
15 | margin ($headerHeight/2 - @height/2) 10px
16 | .brand-logo
17 | font-size 1.8rem
18 | font-weight 300
19 | .header-user-name
20 | &:hover
21 | background none
22 | .header-menus
23 | > li
24 | margin-left 8px
25 |
--------------------------------------------------------------------------------
/source/app/components/header/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import HeaderController from './header.controller';
4 | import headerHtml from './header.jade';
5 | import common from '../_common';
6 |
7 | const header = angular.module('app.components.header', [common.name])
8 | .controller(HeaderController.name, HeaderController);
9 |
10 | export {header, headerHtml, HeaderController};
11 |
--------------------------------------------------------------------------------
/source/app/components/home-hero/home-hero.directive.js:
--------------------------------------------------------------------------------
1 | import homeHeroHtml from './home-hero.jade';
2 |
3 | function HomeHeroDirective () {
4 | return {
5 | restrict: 'AE',
6 | scope: {
7 | getStartedLink: '@'
8 | },
9 | template: homeHeroHtml
10 | };
11 | }
12 |
13 | HomeHeroDirective.$inject = [];
14 |
15 | export default HomeHeroDirective;
16 |
--------------------------------------------------------------------------------
/source/app/components/home-hero/home-hero.directive.spec.js:
--------------------------------------------------------------------------------
1 | import HomeHeroDirective from './home-hero.directive';
2 |
3 | describe('HomeHero Directive', () => {
4 | let element;
5 |
6 | beforeEach(() => {
7 | angular.module('test', [])
8 | .directive('aioHomeHero', HomeHeroDirective);
9 | angular.mock.module('test');
10 | });
11 |
12 | beforeEach(() => {
13 | angular.mock.inject(($compile, $rootScope) => {
14 | const scope = $rootScope.$new();
15 | const tempalte = '';
16 | element = $compile(tempalte)(scope);
17 | scope.$digest();
18 | });
19 | });
20 |
21 | it('should populate the correct link in template', () => {
22 | expect(element.find('.btn-get-started').attr('ui-sref')).toEqual('test.link');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/source/app/components/home-hero/home-hero.jade:
--------------------------------------------------------------------------------
1 | - require('./home-hero.styl')
2 | .home-hero-view
3 | .card-panel.blue.darken-3.center-align.p3
4 | h4.title.white-text Aio Angular App
5 | h5.subtitle.grey-text.text-lighten-1
6 | | Awesome web app built on AngularJS & Material Design.
7 | a.btn-get-started.btn.btn-large.green.waves-effect.waves-light.mt2(ui-sref="{{getStartedLink}}") Get Started
8 |
--------------------------------------------------------------------------------
/source/app/components/home-hero/home-hero.styl:
--------------------------------------------------------------------------------
1 | @import '../../components/_common/styles/common'
2 |
3 | .home-hero-view
4 | .card-panel
5 | .title
6 | font-weight 300
7 | padding-top 150px
8 | background url("../../components/_layout/logo.png") top center no-repeat
9 | .subtitle
10 | font-weight 300
11 |
--------------------------------------------------------------------------------
/source/app/components/home-hero/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import HomeHeroDirective from './home-hero.directive';
4 |
5 | const homeHero = angular.module('app.components.homeHero', [])
6 | .directive('aioHomeHero', HomeHeroDirective);
7 |
8 | export default homeHero;
9 |
--------------------------------------------------------------------------------
/source/app/components/loading/config.js:
--------------------------------------------------------------------------------
1 | function appLoadingConfig (cfpLoadingBarProvider) {
2 | cfpLoadingBarProvider.includeSpinner = false;
3 | cfpLoadingBarProvider.loadingBarTemplate = [
4 | ''
7 | ].join('');
8 | }
9 |
10 | appLoadingConfig.$inject = ['cfpLoadingBarProvider'];
11 |
12 | export default appLoadingConfig;
13 |
--------------------------------------------------------------------------------
/source/app/components/loading/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 | import ngLoadingBar from 'angular-loading-bar';
3 |
4 | import appLoadingConfig from './config';
5 | import './loading.styl';
6 |
7 | const loading = angular.module('app.components.loading', [
8 | ngLoadingBar
9 | ])
10 | .config(appLoadingConfig);
11 |
12 | export default loading;
13 |
--------------------------------------------------------------------------------
/source/app/components/loading/loading.styl:
--------------------------------------------------------------------------------
1 | @import '../../components/_common/styles/common'
2 |
3 | .loading-view
4 | width 100%
5 | z-index 1000
6 | position fixed
7 | top 0
8 | left 0
9 | .progress
10 | box-shadow: 0 0 5px $loadingBarShodowColor
11 | margin 0
12 | background-color $loadingBarColor
13 | .indeterminate
14 | background-color $loadingBgColor
15 |
--------------------------------------------------------------------------------
/source/app/components/login-form/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import LoginFormController from './login-form.controller';
4 | import LoginFormDirective from './login-form.directive';
5 |
6 | const loginForm = angular.module('app.components.loginForm', [])
7 | .controller(LoginFormController.name, LoginFormController)
8 | .directive(`aioLoginForm`, LoginFormDirective);
9 |
10 | export default loginForm;
11 |
--------------------------------------------------------------------------------
/source/app/components/login-form/login-form.controller.js:
--------------------------------------------------------------------------------
1 | class LoginFormController {
2 | constructor (UserAPI, $state) {
3 | Object.assign(this, {UserAPI, $state});
4 | }
5 |
6 | login (credential) {
7 | const self = this;
8 | if (this.loginForm.$invalid) {
9 | return;
10 | }
11 | this.loginError = null;
12 | this.isRequest = true;
13 | this.UserAPI.login(credential.email, credential.password)
14 | .then(_success)
15 | .catch(_error);
16 |
17 | function _success () {
18 | // user was redirect to login page from other page
19 | if (self.$state.prev) {
20 | self.$state.go(self.$state.prev.state, self.$state.prev.params);
21 | self.$state.prev = null;
22 | } else {
23 | self.$state.go(self.routeAfterLogin);
24 | }
25 | }
26 |
27 | function _error (message) {
28 | self._setError('error', message.text);
29 | self.isRequest = false;
30 | }
31 | }
32 |
33 | _setError (type, text) {
34 | this.loginError = {
35 | type,
36 | text
37 | };
38 | }
39 | }
40 |
41 | LoginFormController.$inject = ['UserAPI', '$state'];
42 |
43 | export default LoginFormController;
44 |
--------------------------------------------------------------------------------
/source/app/components/login-form/login-form.controller.spec.js:
--------------------------------------------------------------------------------
1 | import LoginFormController from './login-form.controller';
2 |
3 | describe('LoginForm Controller', () => {
4 | let controller;
5 | let UserAPI;
6 | let $state;
7 | let $q;
8 | let $rootScope;
9 |
10 | beforeEach(() => {
11 | angular.mock.inject((_$q_, _$rootScope_) => {
12 | $q = _$q_;
13 | $rootScope = _$rootScope_;
14 | UserAPI = jasmine.createSpyObj('UserAPI', ['login']);
15 | $state = jasmine.createSpyObj('$state', ['go']);
16 | controller = new LoginFormController(UserAPI, $state);
17 | });
18 | });
19 |
20 | describe('constructor function', () => {
21 | it('should init successfully', () => {
22 | expect(controller.UserAPI).toBe(UserAPI);
23 | expect(controller.$state).toBe($state);
24 | });
25 | });
26 |
27 | describe('login function', () => {
28 | let deferred;
29 | let credential;
30 |
31 | beforeEach(() => {
32 | credential = {
33 | email: 'email',
34 | password: 'password'
35 | };
36 | deferred = $q.defer();
37 | UserAPI.login.and.returnValue(deferred.promise);
38 | controller.loginForm = {};
39 | });
40 |
41 | it('should directly return when form is invalid', () => {
42 | controller.loginForm.$invalid = true;
43 | controller.login(credential);
44 | expect(UserAPI.login).not.toHaveBeenCalled();
45 | });
46 |
47 | describe('success callback', () => {
48 | beforeEach(() => {
49 | deferred.resolve();
50 | controller.login(credential);
51 | });
52 |
53 | it('should go to previous page after login', () => {
54 | $state.prev = {
55 | state: 'state',
56 | params: 'params'
57 | };
58 | $rootScope.$digest();
59 | expect($state.go).toHaveBeenCalledWith('state', 'params');
60 | expect($state.prev).toBe(null);
61 | });
62 |
63 | it('should go to normal page after login', () => {
64 | $state.prev = null;
65 | controller.routeAfterLogin = 'routeAfterLogin';
66 | $rootScope.$digest();
67 | expect($state.go).toHaveBeenCalledWith('routeAfterLogin');
68 | });
69 | });
70 |
71 | it('should call error function with invalid credential', () => {
72 | controller.loginForm.$invalid = false;
73 | spyOn(controller, '_setError');
74 | deferred.reject({text: 'text'});
75 | controller.login(credential);
76 | $rootScope.$digest();
77 | expect(controller._setError).toHaveBeenCalledWith('error', 'text');
78 | expect(controller.isRequest).toBe(false);
79 | });
80 | });
81 |
82 | describe('_setError function', () => {
83 | it('should set passed in parameters to controller', () => {
84 | controller._setError('type', 'text');
85 | expect(controller.loginError.type).toEqual('type');
86 | expect(controller.loginError.text).toEqual('text');
87 | });
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/source/app/components/login-form/login-form.directive.js:
--------------------------------------------------------------------------------
1 | import loginFormHtml from './login-form.jade';
2 | import LoginFormController from './login-form.controller';
3 |
4 | function LoginFormDirective () {
5 | return {
6 | restrict: 'AE',
7 | scope: {},
8 | controller: LoginFormController.name,
9 | controllerAs: 'form',
10 | bindToController: {
11 | needCheckLogin: '=',
12 | userInfo: '=',
13 | routeAfterLogin: '@',
14 | loginError: '='
15 | },
16 | template: loginFormHtml
17 | };
18 | }
19 |
20 | export default LoginFormDirective;
21 |
--------------------------------------------------------------------------------
/source/app/components/login-form/login-form.jade:
--------------------------------------------------------------------------------
1 | - require('./login-form.styl')
2 | .login-form-view
3 | .login-checking.white-text(ng-if="form.needCheckLogin")
4 | i.medium(ng-class="{'mdi-notification-sync icon-rotate-animation': !form.userInfo, 'mdi-action-account-box': form.userInfo}")
5 | p.loading-text {{form.userInfo ? form.userInfo.name : 'Checking Login User...'}}
6 |
7 | form.card(name="form.loginForm", ng-submit="form.login(form.credential)", novalidate, autocomplete="off")
8 | .card-title.black-text.p2
9 | | Please sign in with your Email and Password
10 | .login-message.p1(class="{{form.loginError.type}}", ng-if="form.loginError")
11 | p.white-text {{form.loginError.text}}
12 | .card-content.p3
13 | .input-field
14 | i.mdi-communication-email.prefix
15 | input(name="email", type="email", ng-model="form.credential.email", required, aio-focus-me="true")
16 | label(for="email") Email
17 | .input-field
18 | i.mdi-communication-vpn-key.prefix
19 | input(name="password" type="password", ng-model="form.credential.password", required)
20 | label(for="password") Password
21 | .card-actions.center-align.p2
22 | button.btn.btn-large.blue.waves-effect.waves-light.btn-login(type="submit",
23 | ng-disabled="form.loginForm.$invalid || form.isRequest",
24 | ng-class="{'disabled': form.loginForm.$invalid || form.isRequest}",
25 | aio-loading-button="form.isRequest"
26 | ) Sign In
27 |
--------------------------------------------------------------------------------
/source/app/components/login-form/login-form.styl:
--------------------------------------------------------------------------------
1 | @import '../_common/styles/common'
2 |
3 | .login-form-view
4 | .login-checking
5 | display flex
6 | flex-direction column
7 | justify-content center
8 | align-items center
9 | z-index 10
10 | background rgba(0, 0, 0, .6)
11 | text-align center
12 | position absolute
13 | width 100%
14 | height 100%
15 | .loading-text
16 | animation pulse .8s linear infinite
17 | font-size 20px
18 | margin 0
19 | .card
20 | margin 0
21 | .card-title
22 | border-bottom 1px solid rgba(0, 0, 0, .2)
23 | .login-message
24 | &.error
25 | background-color $errorColor
26 | &.success
27 | background-color $successColor
28 | p
29 | font-size 18px
30 | margin-top 0
31 |
--------------------------------------------------------------------------------
/source/app/components/modal/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import './modal.styl';
4 | import ModalService from './modal.service';
5 |
6 | const modal = angular.module('app.components.modal', [])
7 | .service('Modal', ModalService);
8 |
9 | export default modal;
10 |
--------------------------------------------------------------------------------
/source/app/components/modal/modal.service.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | class ModalService {
4 | constructor ($timeout) {
5 | Object.assign(this, {$timeout});
6 | }
7 |
8 | open (title, content, buttons, callback) {
9 | const modalView = angular.element('', {
10 | class: 'modal-view'
11 | });
12 | const modalDiv = angular.element('', {
13 | id: 'modal',
14 | class: 'modal'
15 | });
16 | modalDiv
17 | .append(this._buildHeaderAndContent(title, content))
18 | .append(this._buildFooter(buttons, callback));
19 | modalView
20 | .append(modalDiv)
21 | .appendTo('body');
22 | this.$timeout(() => {
23 | $('#modal').openModal({
24 | dismissible: false
25 | });
26 | }, 100);
27 | }
28 |
29 | _buildHeaderAndContent (title, content) {
30 | const headerDiv = angular.element('', {
31 | class: 'modal-content'
32 | });
33 | // title
34 | const titleDiv = angular.element('', {
35 | text: title
36 | });
37 | // content
38 | const contentDiv = angular.element('', {
39 | text: content
40 | });
41 | headerDiv.append(titleDiv).append(contentDiv);
42 | return headerDiv;
43 | }
44 |
45 | _buildFooter (buttons, callback) {
46 | const footerDiv = angular.element('', {
47 | class: 'modal-footer'
48 | });
49 | // ok button
50 | const okBtn = angular.element('', {
51 | class: 'btn btn-ok modal-action modal-close waves-effect waves-light mr3',
52 | text: buttons.ok
53 | });
54 | okBtn.on('click', () => {
55 | if (angular.isDefined(callback)) {
56 | callback(true);
57 | }
58 | this._close();
59 | });
60 | footerDiv.append(okBtn);
61 | // cancel button
62 | if (angular.isDefined(buttons.cancel)) {
63 | const cancelBtn = angular.element('', {
64 | class: 'btn btn-cancel white black-text modal-action modal-close waves-effect waves-light mr2',
65 | text: buttons.cancel
66 | });
67 | footerDiv.append(cancelBtn);
68 | cancelBtn.on('click', () => {
69 | if (angular.isDefined(callback)) {
70 | callback(false);
71 | }
72 | this._close();
73 | });
74 | }
75 | return footerDiv;
76 | }
77 |
78 | _close () {
79 | this.$timeout(() => {
80 | angular.element('.modal-view').remove();
81 | }, 100);
82 | }
83 | }
84 |
85 | ModalService.$inject = ['$timeout'];
86 |
87 | export default ModalService;
88 |
--------------------------------------------------------------------------------
/source/app/components/modal/modal.styl:
--------------------------------------------------------------------------------
1 | @import '../_common/styles/common'
2 |
3 | .modal-view
4 | .modal
5 | .modal-footer
6 | a
7 | &:first-child
8 | margin-right 16px
9 | &:last-child
10 | margin-right 24px
11 |
--------------------------------------------------------------------------------
/source/app/components/phone-form/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import PhoneFormController from './phone-form.controller';
4 | import PhoneFromDirective from './phone-form.directive';
5 |
6 | const phoneForm = angular.module('app.components.phoneForm', [])
7 | .controller(PhoneFormController.name, PhoneFormController)
8 | .directive(`aioPhoneForm`, PhoneFromDirective);
9 |
10 | export default phoneForm;
11 |
--------------------------------------------------------------------------------
/source/app/components/phone-form/phone-form.controller.js:
--------------------------------------------------------------------------------
1 | class PhoneFormController {
2 | constructor () {
3 | this.allOS = ['Android', 'iOS', 'Windows Phone'];
4 | }
5 | submitForm (phone) {
6 | if (this.phoneForm.$invalid || !this.phone.releaseDate) {
7 | return;
8 | }
9 | this.isRequest = true;
10 | // call submit method passed in from outer scope
11 | this.submit({phone})
12 | .then(() => {
13 | this._endRequest();
14 | this.phoneForm.$setPristine();
15 | })
16 | .catch(this._endRequest.bind(this));
17 | }
18 |
19 | _endRequest () {
20 | this.isRequest = false;
21 | }
22 | }
23 |
24 | PhoneFormController.$inject = [];
25 |
26 | export default PhoneFormController;
27 |
--------------------------------------------------------------------------------
/source/app/components/phone-form/phone-form.controller.spec.js:
--------------------------------------------------------------------------------
1 | import PhoneFormController from './phone-form.controller';
2 |
3 | describe('PhoneForm Controller', () => {
4 | let controller;
5 | let $q;
6 | let $rootScope;
7 |
8 | beforeEach(() => {
9 | angular.mock.inject((_$q_, _$rootScope_) => {
10 | $q = _$q_;
11 | $rootScope = _$rootScope_;
12 | controller = new PhoneFormController();
13 | });
14 | });
15 |
16 | describe('constructor function', () => {
17 | it('should init successfully', () => {
18 | expect(controller.allOS).toEqual(['Android', 'iOS', 'Windows Phone']);
19 | });
20 | });
21 |
22 | describe('submitForm function', () => {
23 | let deferred;
24 | let phone;
25 | beforeEach(() => {
26 | phone = {phone: 'phone'};
27 | deferred = $q.defer();
28 | controller.phoneForm = {};
29 | controller.phone = {};
30 | controller.submit = jasmine.createSpy('submit');
31 | controller.submit.and.returnValue(deferred.promise);
32 | });
33 |
34 | describe('form is invalid', () => {
35 | it('should do nothing if form is invalid', () => {
36 | controller.phoneForm.$invalid = true;
37 | controller.submitForm(phone.phone);
38 | expect(controller.isRequest).not.toBeDefined();
39 | expect(controller.submit).not.toHaveBeenCalled();
40 | });
41 |
42 | it('should do nothing if releaseDate is not set', () => {
43 | controller.phoneForm.$invalid = false;
44 | controller.phone.releaseDate = null;
45 | controller.submitForm(phone.phone);
46 | expect(controller.isRequest).not.toBeDefined();
47 | expect(controller.submit).not.toHaveBeenCalled();
48 | });
49 | });
50 |
51 | describe('form is valid', () => {
52 | beforeEach(() => {
53 | phone = {phone: 'phone'};
54 | controller.phoneForm.$invalid = false;
55 | controller.phone.releaseDate = new Date();
56 | spyOn(controller, '_endRequest');
57 | controller.phoneForm.$setPristine = jasmine.createSpy('setPristine');
58 | });
59 |
60 | it('should set form pristine if submit is resolved', () => {
61 | deferred.resolve();
62 | controller.submitForm(phone.phone);
63 | expect(controller.isRequest).toBe(true);
64 | expect(controller.submit).toHaveBeenCalledWith(phone);
65 | $rootScope.$digest();
66 | expect(controller._endRequest).toHaveBeenCalled();
67 | expect(controller.phoneForm.$setPristine).toHaveBeenCalled();
68 | });
69 |
70 | it('should not set form pristine if submit is rejected', () => {
71 | deferred.reject();
72 | controller.submitForm(phone.phone);
73 | expect(controller.isRequest).toBe(true);
74 | expect(controller.submit).toHaveBeenCalledWith(phone);
75 | $rootScope.$digest();
76 | expect(controller._endRequest).toHaveBeenCalled();
77 | expect(controller.phoneForm.$setPristine).not.toHaveBeenCalled();
78 | });
79 | });
80 | });
81 |
82 | describe('_endRequest function', () => {
83 | it('should end request as expected', () => {
84 | controller._endRequest();
85 | expect(controller.isRequest).toBe(false);
86 | });
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/source/app/components/phone-form/phone-form.directive.js:
--------------------------------------------------------------------------------
1 | import PhoneFormController from './phone-form.controller';
2 | import phoneFormHtml from './phone-form.jade';
3 |
4 | function PhoneFormDirective () {
5 | return {
6 | restrict: 'AE',
7 | scope: {},
8 | controller: PhoneFormController.name,
9 | controllerAs: 'form',
10 | bindToController: {
11 | phone: '=',
12 | state: '@',
13 | submit: '&',
14 | cancel: '&'
15 | },
16 | template: phoneFormHtml
17 | };
18 | }
19 |
20 | PhoneFormDirective.$inject = [];
21 |
22 | export default PhoneFormDirective;
23 |
--------------------------------------------------------------------------------
/source/app/components/phone-form/phone-form.styl:
--------------------------------------------------------------------------------
1 | @import '../_common/styles/common'
2 |
3 | .phone-form-view
4 | .form
5 | display flex
6 | flex-flow row wrap
7 | +sm()
8 | flex-direction column
9 | .form-group
10 | flex 1 1 auto
11 | margin 16px
12 | .form-item
13 | padding 8px
14 | border-bottom 1px dashed rgba(0, 0, 0, 0.12)
15 | .form-label
16 | display flex
17 | span
18 | flex 1 1 auto
19 | .form-value
20 | +sm()
21 | text-align right
22 | .form-field
23 | position relative
24 | display none
25 | .error-message
26 | &.ng-inactive
27 | display none
28 | &.ng-active
29 | display block
30 | border-radius 3px
31 | background-color rgba(0,0,0,.1)
32 | padding 5px
33 | position absolute
34 | right 0
35 | bottom 45px
36 | z-index 1
37 | &:after
38 | content ''
39 | border 7px solid transparent
40 | border-top-color rgba(0,0,0,0.1)
41 | position absolute
42 | right 2px
43 | margin-top 5px
44 | .form-control
45 | flex 1 1 auto
46 | .datepicker
47 | cursor pointer
48 | .dropdown-content
49 | width 100%
50 | .error-icon
51 | position absolute
52 | bottom 15px
53 | right 0
54 |
55 | .actions
56 | display none
57 | border-top 1px solid rgba(0, 0, 0, 0.12)
58 | flex 1 1 100%
59 | text-align center
60 | +sm()
61 | flex 1 1 auto
62 | button
63 | &.btn-cancel
64 | margin-right 50px
65 | +sm()
66 | display block
67 | &.btn-cancel
68 | margin auto
69 | &.btn-save
70 | margin 10px auto 0
71 |
72 | .form-group + .form-group
73 | +sm()
74 | margin-top 0
75 |
76 | &.editing, &.adding
77 | .form-group > .form-item
78 | border-bottom none
79 | .form-label
80 | display none
81 | .form-field
82 | display flex
83 | .actions
84 | display block
85 |
86 | &.adding
87 | .actions
88 | button.btn-cancel
89 | display none
90 |
--------------------------------------------------------------------------------
/source/app/components/phone-table/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import PhoneTableController from './phone-table.controller';
4 | import PhoneTableDirective from './phone-table.directive';
5 |
6 | const phoneTable = angular.module('app.components.phoneTable', [])
7 | .controller(PhoneTableController.name, PhoneTableController)
8 | .directive(`aioPhoneTable`, PhoneTableDirective);
9 |
10 | export default phoneTable;
11 |
--------------------------------------------------------------------------------
/source/app/components/phone-table/phone-table.controller.js:
--------------------------------------------------------------------------------
1 | class PhoneTableController {
2 | constructor () {}
3 | }
4 |
5 | PhoneTableController.$inject = [];
6 |
7 | export default PhoneTableController;
8 |
--------------------------------------------------------------------------------
/source/app/components/phone-table/phone-table.directive.js:
--------------------------------------------------------------------------------
1 | import PhoneTableController from './phone-table.controller';
2 | import phoneTableHtml from './phone-table.jade';
3 |
4 | function PhoneTableDirective () {
5 | return {
6 | restrict: 'AE',
7 | scope: {},
8 | controller: PhoneTableController.name,
9 | controllerAs: 'table',
10 | bindToController: {
11 | phones: '=',
12 | firstButtonClick: '&',
13 | secondButtonClick: '&'
14 | },
15 | template: phoneTableHtml
16 | };
17 | }
18 |
19 | PhoneTableDirective.$inject = [];
20 |
21 | export default PhoneTableDirective;
22 |
--------------------------------------------------------------------------------
/source/app/components/phone-table/phone-table.directive.spec.js:
--------------------------------------------------------------------------------
1 | import PhoneTableDirective from './phone-table.directive';
2 | import PhoneTableController from './phone-table.controller';
3 |
4 | describe('PhoneTable Directive', () => {
5 | let element;
6 | let scope;
7 |
8 | beforeEach(() => {
9 | angular.module('test', [])
10 | .controller('PhoneTableController', PhoneTableController)
11 | .directive('aioPhoneTable', PhoneTableDirective);
12 | angular.mock.module('test');
13 | });
14 |
15 | beforeEach(() => {
16 | angular.mock.inject(($compile, $rootScope) => {
17 | scope = $rootScope.$new();
18 | scope.phones = [
19 | {
20 | id: 1,
21 | model: 'model1',
22 | os: 'os1',
23 | price: 1111
24 | },
25 | {
26 | id: 2,
27 | model: 'model2',
28 | os: 'os2',
29 | price: 2222
30 | }
31 | ];
32 | scope.firstButtonClick = jasmine.createSpy('firstButtonClick');
33 | scope.secondButtonClick = jasmine.createSpy('secondButtonClick');
34 | const template = `
35 |
38 | `;
39 | element = $compile(template)(scope);
40 | scope.$digest();
41 | });
42 | });
43 |
44 | it('should populate tempalte correctly', () => {
45 | const phoneItems = element.find('.phone-item');
46 | expect(phoneItems.length).toEqual(2);
47 | expect($(phoneItems[0]).find('td:eq(0)').text()).toEqual('model1');
48 | expect($(phoneItems[0]).find('td:eq(1)').text()).toEqual('os1');
49 | expect($(phoneItems[0]).find('td:eq(2)').text()).toEqual('1111');
50 | expect($(phoneItems[1]).find('td:eq(0)').text()).toEqual('model2');
51 | expect($(phoneItems[1]).find('td:eq(1)').text()).toEqual('os2');
52 | expect($(phoneItems[1]).find('td:eq(2)').text()).toEqual('2222');
53 | });
54 |
55 | it('should call first callback when clicking first button', () => {
56 | const phoneItems = element.find('.phone-item');
57 | $(phoneItems[0]).find('.btn-first').click();
58 | expect(scope.firstButtonClick).toHaveBeenCalled();
59 | expect(scope.firstButtonClick.calls.argsFor(0)[1]).toEqual(scope.phones[0]);
60 | });
61 |
62 | it('should call second callback when clicking second button', () => {
63 | const phoneItems = element.find('.phone-item');
64 | $(phoneItems[1]).find('.btn-second').click();
65 | expect(scope.secondButtonClick).toHaveBeenCalled();
66 | expect(scope.secondButtonClick.calls.argsFor(0)[1]).toEqual(scope.phones[1]);
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/source/app/components/phone-table/phone-table.jade:
--------------------------------------------------------------------------------
1 | - require('./phone-table.styl')
2 | .phone-table-view
3 | .card-panel.mt2
4 | table.highlight
5 | thead.heading
6 | tr
7 | th Model
8 | th.hide-sm OS
9 | th.hide-sm Price
10 | th.center-align Action
11 | tbody
12 | tr.empty-row(ng-if="table.phones.length === 0")
13 | td(colspan="4") No Phones found!
14 | tr.phone-item(ng-repeat="phone in table.phones track by phone.id")
15 | td
16 | span {{::phone.model}}
17 | td.hide-sm
18 | span {{::phone.os}}
19 | td.hide-sm
20 | span {{::phone.price}}
21 | td.center-align
22 | button.btn-first.btn-floating.blue.waves-light.waves-effect.tooltipped.mr1(
23 | ng-click="table.firstButtonClick({phone: phone})",
24 | data-position="top", data-delay="50",
25 | data-tooltip="View", aio-tooltip-init
26 | )
27 | i.mdi-action-info
28 | button.btn-second.btn-floating.red.waves-effect.waves-light.tooltipped(
29 | ng-click="table.secondButtonClick({phone: phone})",
30 | data-position="top", data-delay="50",
31 | data-tooltip="Delete", aio-tooltip-init
32 | )
33 | i.mdi-action-delete
34 |
--------------------------------------------------------------------------------
/source/app/components/phone-table/phone-table.styl:
--------------------------------------------------------------------------------
1 | @import '../_common/styles/common'
2 |
3 | .phone-table-view
4 | table
5 | .heading
6 | th
7 | font-weight bold
8 | +sm()
9 | text-align center
10 | .phone-item
11 | &.ng-enter
12 | animation slideInRight .8s
13 | &.ng-leave
14 | animation slideOutRight .8s
15 |
--------------------------------------------------------------------------------
/source/app/components/sidebar-sm/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import SidebarSmController from './sidebar-sm.controller';
4 | import sidebarSmHtml from './sidebar-sm.jade';
5 | import common from '../_common';
6 |
7 | const sidebarSm = angular.module('app.components.sidebarSm', [
8 | common.name
9 | ])
10 | .controller(SidebarSmController.name, SidebarSmController);
11 |
12 | export default {sidebarSm, sidebarSmHtml, SidebarSmController};
13 |
--------------------------------------------------------------------------------
/source/app/components/sidebar-sm/sidebar-sm.controller.js:
--------------------------------------------------------------------------------
1 | class SidebarSmController {
2 | constructor ($rootScope) {
3 | Object.assign(this, {$rootScope});
4 | }
5 |
6 | toggleSidebar (flag) {
7 | if (typeof flag === 'undefined') {
8 | this.$rootScope.showSidebar = !this.$rootScope.showSidebar;
9 | } else {
10 | this.$rootScope.showSidebar = flag;
11 | }
12 | }
13 | }
14 |
15 | SidebarSmController.$inject = ['$rootScope'];
16 |
17 | export default SidebarSmController;
18 |
--------------------------------------------------------------------------------
/source/app/components/sidebar-sm/sidebar-sm.controller.spec.js:
--------------------------------------------------------------------------------
1 | import SidebarSmController from './sidebar-sm.controller';
2 |
3 | describe('SidebarSm Controller', () => {
4 | let controller;
5 | let $rootScope;
6 |
7 | beforeEach(() => {
8 | $rootScope = {};
9 | controller = new SidebarSmController($rootScope);
10 | });
11 |
12 | describe('constructor function', () => {
13 | it('should init successfully', () => {
14 | expect(controller.$rootScope).toBe($rootScope);
15 | });
16 | });
17 |
18 | describe('toggleSidebar function', () => {
19 | it('should set showSidebar to $rootScope', () => {
20 | controller.toggleSidebar();
21 | expect($rootScope.showSidebar).toBe(true);
22 | controller.toggleSidebar(true);
23 | expect($rootScope.showSidebar).toBe(true);
24 | });
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/source/app/components/sidebar-sm/sidebar-sm.jade:
--------------------------------------------------------------------------------
1 | - require('./sidebar-sm.styl')
2 | a.sidebar-menu-fab.btn-floating.waves-light.waves-effect.red.hide-gt-sm(ng-if="hasSidebar", ng-click="vm.toggleSidebar()")
3 | i.mdi-image-dehaze
4 | .sidebar-backdrop.hide-gt-sm(ng-show="showSidebar", ng-click="vm.toggleSidebar(false)")
5 |
--------------------------------------------------------------------------------
/source/app/components/sidebar-sm/sidebar-sm.styl:
--------------------------------------------------------------------------------
1 | @import '../_common/styles/common'
2 |
3 | .sidebar-menu-fab
4 | position fixed
5 | opactiy 0.7
6 | bottom 15px
7 | right 15px
8 | z-index 999
9 |
10 | .sidebar-backdrop
11 | +sm()
12 | position absolute
13 | top 0
14 | z-index 990
15 | background-color rgba(0, 0, 0, 0.5)
16 | height 100%
17 | width 100%
18 |
--------------------------------------------------------------------------------
/source/app/components/sidebar/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import SidebarController from './sidebar.controller';
4 | import sidebarHtml from './sidebar.jade';
5 | import common from '../_common';
6 |
7 | const sidebar = angular.module('app.components.sidebar', [
8 | common.name
9 | ])
10 | .controller(SidebarController.name, SidebarController);
11 |
12 | export default {sidebar, sidebarHtml, SidebarController};
13 |
--------------------------------------------------------------------------------
/source/app/components/sidebar/sidebar.controller.js:
--------------------------------------------------------------------------------
1 | class SidebarController {
2 | constructor (RouterHelper, $scope, $rootScope) {
3 | Object.assign(this, {RouterHelper, $scope, $rootScope});
4 |
5 | // generate sidebar nav menus
6 | this.navs = this._getNavMenus();
7 | // tell others we have sidebar
8 | this.$rootScope.hasSidebar = true;
9 | this.$scope.$on('$destroy', () => {
10 | this.$rootScope.hasSidebar = false;
11 | });
12 | }
13 |
14 | hideSidebar () {
15 | this.$rootScope.showSidebar = false;
16 | }
17 |
18 | _getNavMenus () {
19 | const navs = [];
20 | const allStates = this.RouterHelper.getStates();
21 | allStates.forEach((state) => {
22 | if (state.sidebar) {
23 | const nav = state.sidebar;
24 | nav.link = state.name;
25 | navs.push(nav);
26 | }
27 | });
28 | return navs;
29 | }
30 | }
31 |
32 | SidebarController.$inject = ['RouterHelper', '$scope', '$rootScope'];
33 |
34 | export default SidebarController;
35 |
--------------------------------------------------------------------------------
/source/app/components/sidebar/sidebar.controller.spec.js:
--------------------------------------------------------------------------------
1 | import SidebarController from './sidebar.controller';
2 |
3 | describe('Sidebar Controller', () => {
4 | let controller;
5 | let $rootScope;
6 | let $scope;
7 | let RouterHelper;
8 |
9 | beforeEach(() => {
10 | $rootScope = {};
11 | $scope = jasmine.createSpyObj('$scope', ['$on']);
12 | RouterHelper = jasmine.createSpyObj('RouterHelper', ['getStates']);
13 | RouterHelper.getStates.and.returnValue([
14 | {
15 | sidebar: {
16 | text: 'sidebar1'
17 | },
18 | name: 'name1'
19 | },
20 | {
21 | name: 'name2'
22 | }
23 | ]);
24 | controller = new SidebarController(RouterHelper, $scope, $rootScope);
25 | });
26 |
27 | describe('constructor function', () => {
28 | it('should init successfully', () => {
29 | expect(controller.RouterHelper).toBe(RouterHelper);
30 | expect(controller.$scope).toBe($scope);
31 | expect(controller.$rootScope).toBe($rootScope);
32 | expect(controller.navs.length).toEqual(1);
33 | expect(controller.navs[0].text).toEqual('sidebar1');
34 | expect(controller.navs[0].link).toEqual('name1');
35 | expect($rootScope.hasSidebar).toBe(true);
36 | expect($scope.$on).toHaveBeenCalled();
37 | expect($scope.$on.calls.argsFor(0)[0]).toEqual('$destroy');
38 | const callback = $scope.$on.calls.argsFor(0)[1];
39 | callback();
40 | expect($rootScope.hasSidebar).toBe(false);
41 | });
42 | });
43 |
44 | describe('hideSidebar function', () => {
45 | it('should set hideSidebar on $rootScope', () => {
46 | controller.hideSidebar();
47 | expect($rootScope.showSidebar).toBe(false);
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/source/app/components/sidebar/sidebar.jade:
--------------------------------------------------------------------------------
1 | - require('./sidebar.styl')
2 | .sidebar
3 | ul.sidebar-menu.p1
4 | li.menu-item.p1(ng-repeat="nav in ::vm.navs", ui-sref-active="active")
5 | a.btn-flat.waves-effect.waves-light.link.pl1(ui-sref="{{::nav.link}}", ng-click="vm.hideSidebar()")
6 | i.left(class="{{::nav.icon}}")
7 | | {{::nav.text}}
8 |
--------------------------------------------------------------------------------
/source/app/components/sidebar/sidebar.styl:
--------------------------------------------------------------------------------
1 | @import '../_common/styles/common'
2 |
3 | .sidebar-view
4 | background-color white
5 | border-right 1px solid rgba(0, 0, 0, 0.2)
6 | +gt-sm()
7 | &.ng-enter
8 | animation slideInLeft .8s
9 | +sm()
10 | position fixed
11 | height 100%
12 | width $sidebarWidth
13 | box-shadow 3px 0 6px rgba(0, 0, 0, 0.2)
14 | transform translateX(-1 * $sidebarWidth)
15 | transition box-shadow, transform .6s cubic-bezier(0.23, 1, 0.32, 1)
16 | z-index 999
17 | &.show
18 | transform none
19 | .sidebar
20 | width $sidebarWidth
21 | .sidebar-menu
22 | list-style none
23 | .menu-item
24 | border-bottom 1px solid rgba(0, 0, 0, 0.2)
25 | i
26 | color #9e9e9e
27 | &.active
28 | border-radius 5px
29 | background-color rgba(146, 205, 255, 0.42)
30 | i
31 | color #4CAF50
32 | &:hover
33 | background-color rgba(146, 205, 255, 0.42)
34 | &:hover
35 | text-shadow 2px 2px rgba(0, 0, 0, 0.2)
36 | background-color #eee
37 | .link
38 | width 100%
39 |
--------------------------------------------------------------------------------
/source/app/components/square-menu/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import SquareMenuDirective from './square-menu.directive';
4 |
5 | const squareMenu = angular.module('app.components.squareMenu', [])
6 | .directive('aioSquareMenu', SquareMenuDirective);
7 |
8 | export default squareMenu;
9 |
--------------------------------------------------------------------------------
/source/app/components/square-menu/square-menu.directive.js:
--------------------------------------------------------------------------------
1 | import squareMenuHtml from './square-menu.jade';
2 |
3 | function SquareMenuDirective () {
4 | return {
5 | restrict: 'AE',
6 | scope: {
7 | menus: '=',
8 | colors: '='
9 | },
10 | template: squareMenuHtml
11 | };
12 | }
13 |
14 | SquareMenuDirective.$inject = [];
15 |
16 | export default SquareMenuDirective;
17 |
--------------------------------------------------------------------------------
/source/app/components/square-menu/square-menu.directive.spec.js:
--------------------------------------------------------------------------------
1 | import SquareMenuDirective from './square-menu.directive';
2 |
3 | describe('SquareMenu Directive', () => {
4 | let element;
5 | let scope;
6 |
7 | beforeEach(() => {
8 | angular.module('test', [])
9 | .directive('aioSquareMenu', SquareMenuDirective);
10 | angular.mock.module('test');
11 | });
12 |
13 | beforeEach(() => {
14 | angular.mock.inject(($rootScope, $compile) => {
15 | scope = $rootScope.$new();
16 | scope.menuList = [
17 | {
18 | link: 'link1',
19 | icon: 'icon1',
20 | count: 1,
21 | name: 'name1'
22 | },
23 | {
24 | link: 'link2',
25 | icon: 'icon2',
26 | count: 2,
27 | name: 'name2'
28 | },
29 | {
30 | link: 'link3',
31 | icon: 'icon3',
32 | count: 3,
33 | name: 'name3'
34 | },
35 | {
36 | link: 'link4',
37 | icon: 'icon4',
38 | count: 4,
39 | name: 'name4'
40 | }
41 | ];
42 | scope.colorList = ['color0', 'color1', 'color2'];
43 | const template = '';
44 | element = $compile(template)(scope);
45 | scope.$digest();
46 | });
47 | });
48 |
49 | it('should populate template correctly', () => {
50 | const menus = element.find('.box');
51 | for (let i = 0; i < menus.length; i++) {
52 | const menu = angular.element(menus[i]);
53 | expect(menu.attr('ui-sref')).toEqual(`link${i + 1}`);
54 | expect(menu.attr('class')).toMatch(new RegExp(`color${i % 3}`));
55 | expect(menu.find('i').attr('class')).toMatch(new RegExp(`icon${i + 1}`));
56 | expect(menu.find('.count').text()).toEqual(`${i + 1}`);
57 | expect(menu.find('.name').text()).toEqual(`name${i + 1}`);
58 | }
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/source/app/components/square-menu/square-menu.jade:
--------------------------------------------------------------------------------
1 | - require('./square-menu.styl')
2 | .square-menu-view.boxes
3 | a.box.white-text(ng-repeat="menu in ::menus",
4 | ui-sref="{{::menu.link}}",
5 | class="{{::colors[$index % colors.length]}}"
6 | )
7 | i.medium(ng-class="::menu.icon")
8 | span.count {{::menu.count}}
9 | p.name {{::menu.name}}
10 |
--------------------------------------------------------------------------------
/source/app/components/square-menu/square-menu.styl:
--------------------------------------------------------------------------------
1 | @import '../_common/styles/common'
2 |
3 | .square-menu-view.boxes
4 | margin-top 20px
5 | display flex
6 | flex-wrap wrap
7 | justify-content center
8 | align-items center
9 | +sm()
10 | flex-direction column
11 | .box
12 | display block
13 | text-decoration none
14 | padding 10px 20px
15 | border-radius 2px
16 | text-shadow 0 0 2px #eee
17 | margin 20px
18 | width 200px
19 | height 150px
20 | &:hover
21 | box-shadow 2px 2px rgba(132, 143, 203, 0.4)
22 | .count
23 | display inline-block
24 | float right
25 | font-size 2rem
26 | margin-top 15px
27 | .name
28 | margin-top 10px
29 | font-weight bold
30 | text-transform uppercase
31 | margin-left 10px
32 |
--------------------------------------------------------------------------------
/source/app/index.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | //- ng-app is determined by build script
3 | html(lang="en", ng-app= htmlWebpackPlugin.options.appName, ng-strict-di, ng-class="::ieClass")
4 | head
5 | //- This helps the ng-show/ng-hide animations start at the right place.
6 | //- Since Angular has this but needs to load, this gives us the class early.
7 | style
8 | .ng-hide { display: none!important; }
9 | title(ng-bind="title")
10 | meta(charset="utf-8")
11 | meta(http-equiv="X-UA-Compatible", content="IE=edge, chrome=1")
12 | meta(name="viewport", content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no")
13 | base(href="/")
14 | //- babel polyfill for IE
15 | //- conditional comment is not supported from IE 10
16 | //- TODO: how to include this only for IE
17 | script(src="polyfill.js")
18 |
19 | body(ng-class="_class + '-page'")
20 | .wrapper(ui-view)
21 |
--------------------------------------------------------------------------------
/source/app/index.js:
--------------------------------------------------------------------------------
1 | // 3rd dependencies
2 | import 'materialize-css/bin/materialize.css';
3 | import 'materialize-css/bin/materialize.js';
4 | import 'animate.css/animate.css';
5 |
6 | // angular
7 | import angular from 'angular';
8 | import uiRouter from 'angular-ui-router';
9 | import ngAnimate from 'angular-animate';
10 | import ngMessage from 'angular-messages';
11 | import 'oclazyload';
12 |
13 | import layout from './components/_layout';
14 | import loading from './components/loading';
15 | import modal from './components/modal';
16 |
17 | // routes
18 | import homeRoute from './pages/home/home.route';
19 | import loginRoute from './pages/login/login.route';
20 | import dashboardRoute from './pages/dashboard/dashboard.route';
21 | import phoneRoute from './pages/phone/phone.route';
22 | import notfoundRoute from './pages/404/404.route';
23 |
24 | export default angular.module('app', [
25 | 'oc.lazyLoad',
26 | uiRouter,
27 | ngAnimate,
28 | ngMessage,
29 | layout.name,
30 | loading.name,
31 | modal.name,
32 | homeRoute.name,
33 | loginRoute.name,
34 | dashboardRoute.name,
35 | phoneRoute.name,
36 | notfoundRoute.name
37 | ]);
38 |
--------------------------------------------------------------------------------
/source/app/pages/404/404.jade:
--------------------------------------------------------------------------------
1 | .not-found-view
2 | h2.center-align 404 Not Found
3 |
--------------------------------------------------------------------------------
/source/app/pages/404/404.route.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | appNotfoundRun.$inject = ['RouterHelper'];
4 | function appNotfoundRun (RouterHelper) {
5 | const otherwise = '/404';
6 | RouterHelper.configureStates(getStates(), otherwise);
7 | }
8 |
9 | function getStates () {
10 | return [
11 | {
12 | state: 'root.layout.notfound',
13 | config: {
14 | url: '/404',
15 | views: {
16 | 'main@root': {
17 | templateProvider: ['$q', ($q) => {
18 | return $q((resolve) => {
19 | require.ensure([], () => {
20 | resolve(require('./404.jade'));
21 | }, '404');
22 | });
23 | }]
24 | },
25 | 'sidebar@root': {}
26 | },
27 | data: {
28 | title: '404',
29 | _class: 'notfound'
30 | },
31 | breadcrumb: '404',
32 | resolve: {
33 | loadModule: ['$q', '$ocLazyLoad', ($q, $ocLazyLoad) => {
34 | return $q((resolve) => {
35 | require.ensure([], () => {
36 | $ocLazyLoad.load({name: require('./index').name});
37 | resolve();
38 | }, '404');
39 | });
40 | }]
41 | }
42 | }
43 | }
44 | ];
45 | }
46 |
47 | export default angular.module('app.routes.404', [])
48 | .run(appNotfoundRun);
49 |
--------------------------------------------------------------------------------
/source/app/pages/404/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | export default angular.module('app.pages.404', []);
4 |
--------------------------------------------------------------------------------
/source/app/pages/dashboard/dashboard.controller.js:
--------------------------------------------------------------------------------
1 | class DashboardController {
2 | constructor (UserAPI) {
3 | Object.assign(this, {UserAPI});
4 |
5 | this.colors = ['indigo', 'red', 'pink'];
6 |
7 | const userInfo = this.UserAPI.getUserInfo();
8 | this.welcomeMessage = `Welcome ${userInfo.name}!`;
9 | this._getProductsSummary();
10 | }
11 |
12 | _getProductsSummary () {
13 | this.UserAPI.getProductSummary()
14 | .then((data) => {
15 | this.products = data;
16 | this.products.forEach((product) => {
17 | product.link = `root.layout.${product.name}`;
18 | });
19 | });
20 | }
21 | }
22 |
23 | DashboardController.$inject = ['UserAPI'];
24 |
25 | export default DashboardController;
26 |
--------------------------------------------------------------------------------
/source/app/pages/dashboard/dashboard.controller.spec.js:
--------------------------------------------------------------------------------
1 | import DashboardController from './dashboard.controller';
2 |
3 | describe('Dashboard Controller', () => {
4 | let controller;
5 | let UserAPI;
6 | let $q;
7 | let $rootScope;
8 | let deferred;
9 |
10 | beforeEach(() => {
11 | angular.mock.inject((_$q_, _$rootScope_) => {
12 | $q = _$q_;
13 | $rootScope = _$rootScope_;
14 | UserAPI = jasmine.createSpyObj('UserAPI', ['getUserInfo', 'getProductSummary']);
15 | UserAPI.getUserInfo.and.returnValue({
16 | name: 'name'
17 | });
18 | deferred = $q.defer();
19 | UserAPI.getProductSummary.and.returnValue(deferred.promise);
20 | controller = new DashboardController(UserAPI);
21 | });
22 | });
23 |
24 | describe('constructor function', () => {
25 | it('should init successfully', () => {
26 | expect(controller.UserAPI).toBe(UserAPI);
27 | expect(controller.colors).toEqual(['indigo', 'red', 'pink']);
28 | expect(controller.welcomeMessage).toEqual('Welcome name!');
29 | });
30 | });
31 |
32 | describe('_getProductsSummary function', () => {
33 | it('should get product data when API is resolved', () => {
34 | deferred.resolve([
35 | {
36 | name: 'name1'
37 | },
38 | {
39 | name: 'name2'
40 | },
41 | {
42 | name: 'name3'
43 | }
44 | ]);
45 | controller._getProductsSummary();
46 | $rootScope.$digest();
47 | expect(controller.products.length).toEqual(3);
48 | expect(controller.products[0].name).toEqual('name1');
49 | expect(controller.products[0].link).toEqual('root.layout.name1');
50 | expect(controller.products[1].name).toEqual('name2');
51 | expect(controller.products[1].link).toEqual('root.layout.name2');
52 | expect(controller.products[2].name).toEqual('name3');
53 | expect(controller.products[2].link).toEqual('root.layout.name3');
54 | });
55 |
56 | it('should not get product data when API is rejected', () => {
57 | deferred.reject();
58 | controller._getProductsSummary();
59 | $rootScope.$digest();
60 | expect(controller.products).not.toBeDefined();
61 | });
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/source/app/pages/dashboard/dashboard.jade:
--------------------------------------------------------------------------------
1 | - require('./dashboard.styl')
2 | .dashboard-view.full-width
3 | aio-banner(text="vm.welcomeMessage")
4 | aio-square-menu(menus="vm.products", colors="vm.colors")
5 |
--------------------------------------------------------------------------------
/source/app/pages/dashboard/dashboard.route.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | appDashboardRun.$inject = ['RouterHelper'];
4 | function appDashboardRun (RouterHelper) {
5 | RouterHelper.configureStates(getStates());
6 | }
7 |
8 | function getStates () {
9 | return [
10 | {
11 | state: 'root.layout.dashboard',
12 | config: {
13 | url: '/dashboard',
14 | views: {
15 | 'main@root': {
16 | templateProvider: ['$q', ($q) => {
17 | return $q((resolve) => {
18 | require.ensure([], () => {
19 | resolve(require('./dashboard.jade'));
20 | }, 'dashboard');
21 | });
22 | }],
23 | controller: 'DashboardController as vm'
24 | }
25 | },
26 | data: {
27 | title: 'Dashboard',
28 | _class: 'dashboard',
29 | requireLogin: true
30 | },
31 | sidebar: {
32 | icon: 'mdi-action-dashboard',
33 | text: 'Dashboard'
34 | },
35 | breadcrumb: 'Dashboard',
36 | resolve: {
37 | loadModule: ['$q', '$ocLazyLoad', ($q, $ocLazyLoad) => {
38 | return $q((resolve) => {
39 | require.ensure([], () => {
40 | $ocLazyLoad.load({name: require('./index').name});
41 | resolve();
42 | }, 'dashboard');
43 | });
44 | }]
45 | }
46 | }
47 | }
48 | ];
49 | }
50 |
51 | export default angular.module('app.routes.dashboard', [])
52 | .run(appDashboardRun);
53 |
--------------------------------------------------------------------------------
/source/app/pages/dashboard/dashboard.styl:
--------------------------------------------------------------------------------
1 | @import '../../components/_common/styles/common'
2 |
--------------------------------------------------------------------------------
/source/app/pages/dashboard/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import DashboardController from './dashboard.controller';
4 |
5 | import banner from '../../components/banner';
6 | import squareMenu from '../../components/square-menu';
7 |
8 | export default angular.module('app.pages.dashboard', [
9 | banner.name,
10 | squareMenu.name
11 | ])
12 | .controller(DashboardController.name, DashboardController);
13 |
--------------------------------------------------------------------------------
/source/app/pages/home/home.jade:
--------------------------------------------------------------------------------
1 | - require('./home.styl')
2 | .home-view
3 | aio-home-hero(get-started-link="root.layout.login")
4 |
--------------------------------------------------------------------------------
/source/app/pages/home/home.route.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | appHomeRun.$inject = ['RouterHelper'];
4 | function appHomeRun (RouterHelper) {
5 | RouterHelper.configureStates(getStates());
6 | }
7 |
8 | function getStates () {
9 | return [
10 | {
11 | state: 'root.layout.home',
12 | config: {
13 | url: '/',
14 | views: {
15 | 'main@root': {
16 | templateProvider: ['$q', ($q) => {
17 | return $q((resolve) => {
18 | require.ensure([], () => {
19 | resolve(require('./home.jade'));
20 | }, 'home');
21 | });
22 | }]
23 | },
24 | 'sidebar@root': {},
25 | 'breadcrumb@root': {}
26 | },
27 | data: {
28 | title: 'Home',
29 | _class: 'home'
30 | },
31 | resolve: {
32 | loadModule: ['$q', '$ocLazyLoad', ($q, $ocLazyLoad) => {
33 | return $q((resolve) => {
34 | require.ensure([], () => {
35 | $ocLazyLoad.load({name: require('./index').name});
36 | resolve();
37 | }, 'home');
38 | });
39 | }]
40 | }
41 | }
42 | }
43 | ];
44 | }
45 |
46 | export default angular.module('app.routes.home', [])
47 | .run(appHomeRun);
48 |
--------------------------------------------------------------------------------
/source/app/pages/home/home.styl:
--------------------------------------------------------------------------------
1 | @import '../../components/_common/styles/common'
2 |
--------------------------------------------------------------------------------
/source/app/pages/home/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import homeHero from '../../components/home-hero';
4 |
5 | export default angular.module('app.pages.home', [
6 | homeHero.name
7 | ]);
8 |
--------------------------------------------------------------------------------
/source/app/pages/login/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import loginForm from '../../components/login-form';
4 | import LoginController from './login.controller';
5 |
6 | export default angular.module('app.pages.login', [
7 | loginForm.name
8 | ])
9 | .controller(LoginController.name, LoginController);
10 |
--------------------------------------------------------------------------------
/source/app/pages/login/login.controller.js:
--------------------------------------------------------------------------------
1 | class LoginController {
2 | constructor (UserAPI, $state, $timeout) {
3 | Object.assign(this, {UserAPI, $state, $timeout});
4 |
5 | this.routeAfterLogin = 'root.layout.dashboard';
6 |
7 | // handle logout
8 | const action = this.$state.params.action;
9 | if (action === 'logout') {
10 | this.needCheckLogin = false;
11 | this.UserAPI.logout()
12 | .then(() => {
13 | this.loginError = {
14 | type: 'success',
15 | text: 'You have been successfully logged out!'
16 | };
17 | });
18 | } else {
19 | this.userInfo = null;
20 | this.needCheckLogin = true;
21 | const self = this;
22 | // check login status firstly
23 | this.UserAPI.checkLoggedInStatus()
24 | .then((data) => {
25 | self.userInfo = data;
26 | self.$timeout(() => {
27 | self.$state.go(self.routeAfterLogin);
28 | }, 1000);
29 | })
30 | .catch(() => {
31 | self.needCheckLogin = false;
32 | });
33 | }
34 | }
35 | }
36 |
37 | LoginController.$inject = ['UserAPI', '$state', '$timeout'];
38 |
39 | export default LoginController;
40 |
--------------------------------------------------------------------------------
/source/app/pages/login/login.controller.spec.js:
--------------------------------------------------------------------------------
1 | import LoginController from './login.controller';
2 |
3 | describe('Login Controller', () => {
4 | let controller;
5 | let UserAPI;
6 | let $state;
7 | let $timeout;
8 | let $q;
9 | let $rootScope;
10 | let logoutDefer;
11 | let checkLoggedInStatusDefer;
12 |
13 | beforeEach(() => {
14 | angular.mock.inject((_$q_, _$rootScope_) => {
15 | $q = _$q_;
16 | $rootScope = _$rootScope_;
17 | UserAPI = jasmine.createSpyObj('UserAPI', ['logout', 'checkLoggedInStatus']);
18 | $state = jasmine.createSpyObj('$state', ['go']);
19 | $state.params = {};
20 | $timeout = jasmine.createSpy('$timeout');
21 | logoutDefer = $q.defer();
22 | checkLoggedInStatusDefer = $q.defer();
23 | UserAPI.logout.and.returnValue(logoutDefer.promise);
24 | UserAPI.checkLoggedInStatus.and.returnValue(checkLoggedInStatusDefer.promise);
25 | });
26 | });
27 |
28 | describe('constructor function', () => {
29 | it('should init successfully', () => {
30 | controller = new LoginController(UserAPI, $state, $timeout);
31 | expect(controller.UserAPI).toBe(UserAPI);
32 | expect(controller.$state).toBe($state);
33 | expect(controller.$timeout).toBe($timeout);
34 | expect(controller.routeAfterLogin).toEqual('root.layout.dashboard');
35 | });
36 |
37 | describe('handle logout', () => {
38 | beforeEach(() => {
39 | $state.params.action = 'logout';
40 | controller = new LoginController(UserAPI, $state, $timeout);
41 | });
42 |
43 | it('should handle logout with logout parameter', () => {
44 | expect(controller.needCheckLogin).toBe(false);
45 | logoutDefer.resolve();
46 | $rootScope.$digest();
47 | expect(controller.loginError.type).toEqual('success');
48 | expect(controller.loginError.text).toEqual('You have been successfully logged out!');
49 | });
50 |
51 | it('should not show error if logout failed', () => {
52 | logoutDefer.reject();
53 | $rootScope.$digest();
54 | expect(controller.loginError).not.toBeDefined();
55 | });
56 | });
57 |
58 | describe('handle automatic login', () => {
59 | beforeEach(() => {
60 | controller = new LoginController(UserAPI, $state, $timeout);
61 | });
62 |
63 | it('should automatically log user in if user has already logged in', () => {
64 | expect(controller.userInfo).toBe(null);
65 | expect(controller.needCheckLogin).toBe(true);
66 | checkLoggedInStatusDefer.resolve('user info');
67 | $rootScope.$digest();
68 | expect(controller.userInfo).toEqual('user info');
69 | expect($timeout).toHaveBeenCalled();
70 | expect($timeout.calls.argsFor(0)[1]).toEqual(1000);
71 | const callback = $timeout.calls.argsFor(0)[0];
72 | callback();
73 | expect($state.go).toHaveBeenCalledWith('root.layout.dashboard');
74 | });
75 |
76 | it('should not log user in if checkLoggedInStatus rejects', () => {
77 | expect(controller.userInfo).toBe(null);
78 | expect(controller.needCheckLogin).toBe(true);
79 | checkLoggedInStatusDefer.reject();
80 | $rootScope.$digest();
81 | expect(controller.userInfo).toBe(null);
82 | expect(controller.needCheckLogin).toBe(false);
83 | });
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/source/app/pages/login/login.jade:
--------------------------------------------------------------------------------
1 | - require('./login.styl')
2 | .login-view
3 | aio-login-form(
4 | need-check-login="vm.needCheckLogin",
5 | user-info="vm.userInfo",
6 | route-after-login="{{vm.routeAfterLogin}}",
7 | login-error="vm.loginError"
8 | )
9 |
--------------------------------------------------------------------------------
/source/app/pages/login/login.route.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | appLoginRun.$inject = ['RouterHelper'];
4 | function appLoginRun (RouterHelper) {
5 | RouterHelper.configureStates(getStates());
6 | }
7 |
8 | function getStates () {
9 | return [
10 | {
11 | state: 'root.layout.login',
12 | config: {
13 | url: '/login?action',
14 | views: {
15 | 'main@root': {
16 | templateProvider: ['$q', ($q) => {
17 | return $q((resolve) => {
18 | require.ensure([], () => {
19 | resolve(require('./login.jade'));
20 | }, 'login');
21 | });
22 | }],
23 | controller: 'LoginController as vm'
24 | },
25 | 'breadcrumb@root': {},
26 | 'sidebar@root': {}
27 | },
28 | data: {
29 | title: 'Login',
30 | _class: 'login'
31 | },
32 | resolve: {
33 | loadModule: ['$q', '$ocLazyLoad', ($q, $ocLazyLoad) => {
34 | return $q((resolve) => {
35 | require.ensure([], () => {
36 | $ocLazyLoad.load({name: require('./index').name});
37 | resolve();
38 | }, 'login');
39 | });
40 | }]
41 | }
42 | }
43 | }
44 | ];
45 | }
46 |
47 | export default angular.module('app.routes.login', [])
48 | .run(appLoginRun);
49 |
--------------------------------------------------------------------------------
/source/app/pages/login/login.styl:
--------------------------------------------------------------------------------
1 | @import '../../components/_common/styles/common'
2 |
3 | .login-view
4 | position relative
5 |
--------------------------------------------------------------------------------
/source/app/pages/phone/add/phone-add.controller.js:
--------------------------------------------------------------------------------
1 | class PhoneAddController {
2 | constructor (PhoneAPI, $state, $q, Modal) {
3 | Object.assign(this, {PhoneAPI, $state, $q, Modal});
4 |
5 | this.phone = {};
6 | this.state = 'add';
7 | }
8 |
9 | addNewPhone (phone) {
10 | const self = this;
11 | // return promise here to let the phone form controller know the response status
12 | return this.PhoneAPI.addNewPhone(phone)
13 | .then(_success)
14 | .catch(_error);
15 |
16 | function _success () {
17 | self.$state.go('root.layout.phone');
18 | }
19 |
20 | function _error (message) {
21 | self.Modal.open('Add phone error', message.text, {ok: 'OK'});
22 | return self.$q.reject();
23 | }
24 | }
25 | }
26 |
27 | PhoneAddController.$inject = ['PhoneAPI', '$state', '$q', 'Modal'];
28 |
29 | export default PhoneAddController;
30 |
--------------------------------------------------------------------------------
/source/app/pages/phone/add/phone-add.controller.spec.js:
--------------------------------------------------------------------------------
1 | import PhoneAddController from './phone-add.controller';
2 |
3 | describe('PhoneAdd Controller', () => {
4 | let controller;
5 | let PhoneAPI;
6 | let $state;
7 | let $q;
8 | let Modal;
9 | let $rootScope;
10 |
11 | beforeEach(() => {
12 | angular.mock.inject((_$rootScope_, _$q_) => {
13 | $rootScope = _$rootScope_;
14 | $q = _$q_;
15 | PhoneAPI = jasmine.createSpyObj('PhoneAPI', ['addNewPhone']);
16 | $state = jasmine.createSpyObj('$state', ['go']);
17 | Modal = jasmine.createSpyObj('Modal', ['open']);
18 | controller = new PhoneAddController(PhoneAPI, $state, $q, Modal);
19 | });
20 | });
21 |
22 | describe('constructor function', () => {
23 | it('should init successfully', () => {
24 | expect(controller.PhoneAPI).toBe(PhoneAPI);
25 | expect(controller.$state).toBe($state);
26 | expect(controller.$q).toBe($q);
27 | expect(controller.Modal).toBe(Modal);
28 | expect(controller.phone).toEqual({});
29 | expect(controller.state).toEqual('add');
30 | });
31 | });
32 |
33 | describe('addNewPhone function', () => {
34 | let deferred;
35 |
36 | beforeEach(() => {
37 | deferred = $q.defer();
38 | PhoneAPI.addNewPhone.and.returnValue(deferred.promise);
39 | spyOn($q, 'reject');
40 | });
41 |
42 | it('should go to correct state if API resolves', () => {
43 | deferred.resolve();
44 | controller.addNewPhone({id: 1});
45 | expect(PhoneAPI.addNewPhone).toHaveBeenCalledWith({id: 1});
46 | $rootScope.$digest();
47 | expect($state.go).toHaveBeenCalledWith('root.layout.phone');
48 | });
49 |
50 | it('should show error modal if API rejects', () => {
51 | deferred.reject({text: 'error'});
52 | controller.addNewPhone({id: 1});
53 | expect(PhoneAPI.addNewPhone).toHaveBeenCalledWith({id: 1});
54 | $rootScope.$digest();
55 | expect(Modal.open).toHaveBeenCalledWith('Add phone error', 'error', {ok: 'OK'});
56 | expect($q.reject).toHaveBeenCalled();
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/source/app/pages/phone/add/phone-add.jade:
--------------------------------------------------------------------------------
1 | - require('./phone-add.styl')
2 | .phone-add-view.full-width
3 | .card
4 | .card-header.p2
5 | h5.title Add a new phone
6 | .card-content
7 | aio-phone-form(
8 | phone="vm.phone",
9 | submit="vm.addNewPhone(phone)",
10 | state="{{vm.state}}"
11 | )
12 |
--------------------------------------------------------------------------------
/source/app/pages/phone/add/phone-add.styl:
--------------------------------------------------------------------------------
1 | @import '../../../components/_common/styles/common'
2 |
3 | .phone-add-view
4 | .card
5 | .card-header
6 | border-bottom 1px solid rgba(0, 0, 0, 0.12)
--------------------------------------------------------------------------------
/source/app/pages/phone/detail/phone-detail.controller.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | const originalPhone = Symbol();
4 | class PhoneDetailController {
5 | constructor (PhoneAPI, $stateParams, $q, Modal) {
6 | Object.assign(this, {PhoneAPI, $stateParams, $q, Modal});
7 |
8 | this[originalPhone] = {};
9 | this.state = 'view';
10 | const id = $stateParams.id;
11 | if (id) {
12 | this._getPhoneDetail(id);
13 | }
14 | }
15 |
16 | _getPhoneDetail (id) {
17 | this.PhoneAPI.getPhoneDetail(id)
18 | .then((data) => {
19 | this.phone = data;
20 | });
21 | }
22 |
23 | beginEdit () {
24 | this[originalPhone] = angular.copy(this.phone);
25 | this.state = 'edit';
26 | }
27 |
28 | cancelUpdate () {
29 | this.phone = angular.copy(this[originalPhone]);
30 | this.state = 'view';
31 | }
32 |
33 | updatePhone (phone) {
34 | const self = this;
35 | // return promise here to let the phone form controller know the response status
36 | return this.PhoneAPI.updatePhone(phone.id, phone)
37 | .then(_success)
38 | .catch(_error);
39 |
40 | function _success (data) {
41 | self.state = 'view';
42 | self.phone = data;
43 | }
44 |
45 | function _error (message) {
46 | self.Modal.open('Update phone error', message.text, {ok: 'OK'}, () => {
47 | self.cancelUpdate();
48 | });
49 | return self.$q.reject();
50 | }
51 | }
52 |
53 | }
54 |
55 | PhoneDetailController.$inject = ['PhoneAPI', '$stateParams', '$q', 'Modal'];
56 |
57 | export default PhoneDetailController;
58 |
--------------------------------------------------------------------------------
/source/app/pages/phone/detail/phone-detail.controller.spec.js:
--------------------------------------------------------------------------------
1 | import PhoneDetailController from './phone-detail.controller';
2 |
3 | describe('PhoneDetail Controller', () => {
4 | let controller;
5 | let PhoneAPI;
6 | let $stateParams;
7 | let $q;
8 | let Modal;
9 | let $rootScope;
10 | let getPhoneDetailDefer;
11 | let updatePhoneDefer;
12 |
13 | beforeEach(() => {
14 | angular.mock.inject((_$q_, _$rootScope_) => {
15 | $q = _$q_;
16 | $rootScope = _$rootScope_;
17 | PhoneAPI = jasmine.createSpyObj('PhoneAPI', ['getPhoneDetail', 'updatePhone']);
18 | $stateParams = {id: 1};
19 | Modal = jasmine.createSpyObj('Modal', ['open']);
20 | getPhoneDetailDefer = $q.defer();
21 | PhoneAPI.getPhoneDetail.and.returnValue(getPhoneDetailDefer.promise);
22 | });
23 | });
24 |
25 | describe('constructor function', () => {
26 | function assertCommon () {
27 | expect(controller.PhoneAPI).toBe(PhoneAPI);
28 | expect(controller.$stateParams).toBe($stateParams);
29 | expect(controller.$q).toBe($q);
30 | expect(controller.Modal).toBe(Modal);
31 | expect(controller.state).toEqual('view');
32 | }
33 |
34 | it('should init successfully with phone id', () => {
35 | $stateParams = {id: 1};
36 | controller = new PhoneDetailController(PhoneAPI, $stateParams, $q, Modal);
37 | assertCommon();
38 | expect(PhoneAPI.getPhoneDetail).toHaveBeenCalledWith(1);
39 | });
40 |
41 | it('should init successfully without phone id', () => {
42 | $stateParams = {};
43 | controller = new PhoneDetailController(PhoneAPI, $stateParams, $q, Modal);
44 | assertCommon();
45 | expect(PhoneAPI.getPhoneDetail).not.toHaveBeenCalled();
46 | });
47 | });
48 |
49 | describe('_getPhoneDetail function', () => {
50 | beforeEach(() => {
51 | $stateParams = {id: 1};
52 | controller = new PhoneDetailController(PhoneAPI, $stateParams, $q, Modal);
53 | });
54 |
55 | it('should set phone data if API resolves', () => {
56 | getPhoneDetailDefer.resolve('phone');
57 | $rootScope.$digest();
58 | expect(controller.phone).toEqual('phone');
59 | });
60 |
61 | it('should not set phone data if API rejects', () => {
62 | getPhoneDetailDefer.reject();
63 | $rootScope.$digest();
64 | expect(controller.phone).not.toBeDefined();
65 | });
66 | });
67 |
68 | describe('beginEdit/cancelUpdate function', () => {
69 | beforeEach(() => {
70 | controller = new PhoneDetailController(PhoneAPI, $stateParams, $q, Modal);
71 | });
72 |
73 | it('should set form to correct state', () => {
74 | controller.phone = {id: 1};
75 | controller.beginEdit();
76 | expect(controller.state).toEqual('edit');
77 | controller.phone = {id: 3};
78 | controller.cancelUpdate(controller);
79 | expect(controller.state).toEqual('view');
80 | expect(controller.phone).toEqual({id: 1});
81 | });
82 | });
83 |
84 | describe('updatePhone function', () => {
85 | let originalPhone;
86 | let updatedPhone;
87 |
88 | beforeEach(() => {
89 | controller = new PhoneDetailController(PhoneAPI, $stateParams, $q, Modal);
90 | updatePhoneDefer = $q.defer();
91 | PhoneAPI.updatePhone.and.returnValue(updatePhoneDefer.promise);
92 | originalPhone = {id: 1, model: 'model1', price: 1111};
93 | updatedPhone = {id: 1, model: 'model2', price: 2222};
94 | });
95 |
96 | it('should update phone successfully if API resolves', () => {
97 | updatePhoneDefer.resolve(updatedPhone);
98 | controller.updatePhone(originalPhone);
99 | expect(PhoneAPI.updatePhone).toHaveBeenCalledWith(1, originalPhone);
100 | $rootScope.$digest();
101 | expect(controller.state).toEqual('view');
102 | expect(controller.phone).toEqual(updatedPhone);
103 | });
104 |
105 | it('should not update phone if API rejects', () => {
106 | updatePhoneDefer.reject({text: 'error'});
107 | spyOn($q, 'reject');
108 | controller.updatePhone(originalPhone);
109 | expect(PhoneAPI.updatePhone).toHaveBeenCalledWith(1, originalPhone);
110 | $rootScope.$digest();
111 | expect(controller.phone).not.toBeDefined();
112 | expect(Modal.open).toHaveBeenCalled();
113 | expect($q.reject).toHaveBeenCalled();
114 | const args = Modal.open.calls.argsFor(0);
115 | expect(args[0]).toEqual('Update phone error');
116 | expect(args[1]).toEqual('error');
117 | expect(args[2]).toEqual({ok: 'OK'});
118 | const callback = args[3];
119 | spyOn(controller, 'cancelUpdate');
120 | callback();
121 | expect(controller.cancelUpdate).toHaveBeenCalled();
122 | });
123 | });
124 | });
125 |
--------------------------------------------------------------------------------
/source/app/pages/phone/detail/phone-detail.jade:
--------------------------------------------------------------------------------
1 | - require('./phone-detail.styl')
2 | .phone-detail-view.full-width
3 | .card
4 | .card-header.p2
5 | h5.title {{::vm.phone.model}}
6 | button.btn-floating.blue(ng-show="vm.state === 'view'", ng-click="vm.beginEdit()")
7 | i.mdi-image-edit
8 | .card-content
9 | aio-phone-form(
10 | phone="vm.phone",
11 | state="{{vm.state}}",
12 | submit="vm.updatePhone(phone)",
13 | cancel="vm.cancelUpdate()"
14 | )
15 |
--------------------------------------------------------------------------------
/source/app/pages/phone/detail/phone-detail.styl:
--------------------------------------------------------------------------------
1 | @import '../../../components/_common/styles/common'
2 |
3 | .phone-detail-view
4 | .card
5 | .card-header
6 | border-bottom 1px solid rgba(0, 0, 0, 0.12)
7 | .title
8 | display inline-block
9 | button
10 | float right
11 | margin-top 5px
12 |
--------------------------------------------------------------------------------
/source/app/pages/phone/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | import PhoneController from './phone.controller';
4 | import PhoneAddController from './add/phone-add.controller';
5 | import PhoneDetailController from './detail/phone-detail.controller';
6 | import PhoneService from './phone.service';
7 |
8 | import phoneForm from '../../components/phone-form';
9 | import phoneTable from '../../components/phone-table';
10 |
11 | export default angular.module('app.pages.phone', [
12 | phoneForm.name,
13 | phoneTable.name
14 | ])
15 | .controller(PhoneController.name, PhoneController)
16 | .controller(PhoneAddController.name, PhoneAddController)
17 | .controller(PhoneDetailController.name, PhoneDetailController)
18 | .service('PhoneAPI', PhoneService);
19 |
--------------------------------------------------------------------------------
/source/app/pages/phone/phone.controller.js:
--------------------------------------------------------------------------------
1 | class PhoneController {
2 | constructor (PhoneAPI, $state, Modal) {
3 | Object.assign(this, {PhoneAPI, $state, Modal});
4 |
5 | this._getPhoneList();
6 | }
7 |
8 | _getPhoneList () {
9 | this.PhoneAPI.getPhones()
10 | .then((data) => {
11 | this.phones = data;
12 | });
13 | }
14 |
15 | gotoPhoneDetail (phone) {
16 | this.$state.go('root.layout.phone.detail', {id: phone.id});
17 | }
18 |
19 | deletePhone (phone) {
20 | this.Modal.open(
21 | 'Are your sure?',
22 | `All information about [${phone.model}] will be REMOVED!`,
23 | {ok: 'delete', cancel: 'cancel'},
24 | (answer) => {
25 | if (answer) {
26 | this._doDelete(phone.id);
27 | }
28 | }
29 | );
30 | }
31 |
32 | _doDelete (id) {
33 | const self = this;
34 | this.PhoneAPI.removePhone(id)
35 | .then(_success)
36 | .catch(_error);
37 |
38 | function _success () {
39 | self._getPhoneList();
40 | }
41 |
42 | function _error (message) {
43 | self.Modal.open(
44 | 'Delete phone error',
45 | message.text,
46 | {ok: 'OK'},
47 | (answer) => {
48 | if (answer) {
49 | self._getPhoneList();
50 | }
51 | }
52 | );
53 | }
54 | }
55 | }
56 |
57 | PhoneController.$inject = ['PhoneAPI', '$state', 'Modal'];
58 |
59 | export default PhoneController;
60 |
--------------------------------------------------------------------------------
/source/app/pages/phone/phone.controller.spec.js:
--------------------------------------------------------------------------------
1 | import PhoneController from './phone.controller';
2 |
3 | describe('Phone Controller', () => {
4 | let controller;
5 | let PhoneAPI;
6 | let $state;
7 | let Modal;
8 | let $q;
9 | let $rootScope;
10 | let getPhonesDefer;
11 | let removePhoneDefer;
12 |
13 | beforeEach(() => {
14 | angular.mock.inject((_$q_, _$rootScope_) => {
15 | $q = _$q_;
16 | $rootScope = _$rootScope_;
17 | PhoneAPI = jasmine.createSpyObj('PhoneAPI', ['getPhones', 'removePhone']);
18 | $state = jasmine.createSpyObj('$state', ['go']);
19 | Modal = jasmine.createSpyObj('Modal', ['open']);
20 | getPhonesDefer = $q.defer();
21 | PhoneAPI.getPhones.and.returnValue(getPhonesDefer.promise);
22 | removePhoneDefer = $q.defer();
23 | PhoneAPI.removePhone.and.returnValue(removePhoneDefer.promise);
24 | controller = new PhoneController(PhoneAPI, $state, Modal);
25 | });
26 | });
27 |
28 | describe('constructor function', () => {
29 | it('should init successfully', () => {
30 | expect(controller.PhoneAPI).toBe(PhoneAPI);
31 | expect(controller.$state).toBe($state);
32 | expect(controller.Modal).toBe(Modal);
33 | });
34 | });
35 |
36 | describe('_getPhoneList function', () => {
37 | it('should set phone data if API resolves', () => {
38 | getPhonesDefer.resolve('phone');
39 | controller._getPhoneList();
40 | $rootScope.$digest();
41 | expect(controller.phones).toEqual('phone');
42 | });
43 |
44 | it('should not set phone data if API rejects', () => {
45 | getPhonesDefer.reject();
46 | controller._getPhoneList();
47 | $rootScope.$digest();
48 | expect(controller.phones).not.toBeDefined();
49 | });
50 | });
51 |
52 | describe('gotoPhoneDetail function', () => {
53 | it('should go to correct state', () => {
54 | controller.gotoPhoneDetail({id: 1});
55 | expect($state.go).toHaveBeenCalledWith('root.layout.phone.detail', {id: 1});
56 | });
57 | });
58 |
59 | describe('deletePhone function', () => {
60 | beforeEach(() => {
61 | spyOn(controller, '_doDelete');
62 | });
63 |
64 | it('should show error modal before deleting', () => {
65 | controller.deletePhone({
66 | id: 1,
67 | model: 'model'
68 | });
69 | expect(Modal.open).toHaveBeenCalled();
70 | const args = Modal.open.calls.argsFor(0);
71 | expect(args[0]).toEqual('Are your sure?');
72 | expect(args[1]).toEqual('All information about [model] will be REMOVED!');
73 | expect(args[2]).toEqual({ok: 'delete', cancel: 'cancel'});
74 | const callback = args[3];
75 | callback(true);
76 | expect(controller._doDelete).toHaveBeenCalledWith(1);
77 | controller._doDelete.calls.reset();
78 | callback(false);
79 | expect(controller._doDelete).not.toHaveBeenCalled();
80 | });
81 | });
82 |
83 | describe('_doDelete function', () => {
84 | beforeEach(() => {
85 | spyOn(controller, '_getPhoneList');
86 | });
87 |
88 | it('should refresh phone list after removing successfully', () => {
89 | removePhoneDefer.resolve();
90 | controller._doDelete(1);
91 | expect(PhoneAPI.removePhone).toHaveBeenCalledWith(1);
92 | $rootScope.$digest();
93 | expect(controller._getPhoneList).toHaveBeenCalled();
94 | expect(Modal.open).not.toHaveBeenCalled();
95 | });
96 |
97 | it('should show error modal after removing failed', () => {
98 | removePhoneDefer.reject({text: 'error'});
99 | controller._doDelete(1);
100 | expect(PhoneAPI.removePhone).toHaveBeenCalledWith(1);
101 | $rootScope.$digest();
102 | expect(Modal.open).toHaveBeenCalled();
103 | const args = Modal.open.calls.argsFor(0);
104 | expect(args[0]).toEqual('Delete phone error');
105 | expect(args[1]).toEqual('error');
106 | expect(args[2]).toEqual({ok: 'OK'});
107 | const callback = args[3];
108 | callback(true);
109 | expect(controller._getPhoneList).toHaveBeenCalled();
110 | controller._getPhoneList.calls.reset();
111 | callback(false);
112 | expect(controller._getPhoneList).not.toHaveBeenCalled();
113 | });
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/source/app/pages/phone/phone.jade:
--------------------------------------------------------------------------------
1 | .phone-main-view.full-width
2 | a.btn-add-new.btn.btn-large.green.waves-effect.waves-light(ui-sref="root.layout.phone.add")
3 | i.mdi-content-add-box.left
4 | | Add New
5 | aio-phone-table(
6 | phones="vm.phones",
7 | first-button-click="vm.gotoPhoneDetail(phone)",
8 | second-button-click="vm.deletePhone(phone)"
9 | )
10 |
--------------------------------------------------------------------------------
/source/app/pages/phone/phone.route.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 |
3 | appPhoneRun.$inject = ['RouterHelper'];
4 | function appPhoneRun (RouterHelper) {
5 | RouterHelper.configureStates(getStates());
6 | }
7 |
8 | function getStates () {
9 | return [
10 | {
11 | state: 'root.layout.phone',
12 | config: {
13 | url: '/phone',
14 | views: {
15 | 'main@root': {
16 | templateProvider: ['$q', ($q) => {
17 | return $q((resolve) => {
18 | require.ensure([], () => {
19 | resolve(require('./phone.jade'));
20 | }, 'phone');
21 | });
22 | }],
23 | controller: 'PhoneController as vm'
24 | }
25 | },
26 | data: {
27 | title: 'Phone',
28 | _class: 'phone',
29 | requireLogin: true
30 | },
31 | sidebar: {
32 | icon: 'mdi-hardware-phone-android',
33 | text: 'Phones'
34 | },
35 | breadcrumb: 'Phone List',
36 | resolve: {
37 | loadModule: ['$q', '$ocLazyLoad', ($q, $ocLazyLoad) => {
38 | return $q((resolve) => {
39 | require.ensure([], () => {
40 | $ocLazyLoad.load({name: require('./index').name});
41 | resolve();
42 | }, 'phone');
43 | });
44 | }]
45 | }
46 | }
47 | },
48 | {
49 | state: 'root.layout.phone.add',
50 | config: {
51 | url: '/add',
52 | views: {
53 | 'main@root': {
54 | templateProvider: ['$q', ($q) => {
55 | return $q((resolve) => {
56 | require.ensure([], () => {
57 | resolve(require('./add/phone-add.jade'));
58 | }, 'phone');
59 | });
60 | }],
61 | controller: 'PhoneAddController as vm'
62 | }
63 | },
64 | breadcrumb: 'Add Phone'
65 | }
66 | },
67 | {
68 | state: 'root.layout.phone.detail',
69 | config: {
70 | url: '/:id',
71 | views: {
72 | 'main@root': {
73 | templateProvider: ['$q', ($q) => {
74 | return $q((resolve) => {
75 | require.ensure([], () => {
76 | resolve(require('./detail/phone-detail.jade'));
77 | }, 'phone');
78 | });
79 | }],
80 | controller: 'PhoneDetailController as vm'
81 | }
82 | },
83 | breadcrumb: 'Phone Detail'
84 | }
85 | }
86 | ];
87 | }
88 |
89 | export default angular.module('app.routes.phone', [])
90 | .run(appPhoneRun);
91 |
--------------------------------------------------------------------------------
/source/app/pages/phone/phone.service.js:
--------------------------------------------------------------------------------
1 | const errorHandler = Symbol();
2 | class PhoneService {
3 | constructor ($http, $q, AjaxError) {
4 | Object.assign(this, {$http, $q, AjaxError});
5 |
6 | this[errorHandler] = this.AjaxError.catcher.bind(this.AjaxError);
7 | }
8 |
9 | getPhones () {
10 | const self = this;
11 | return this.$http.get('api/phones')
12 | .then(_success)
13 | .catch(this[errorHandler]);
14 |
15 | function _success (response) {
16 | const data = response.data;
17 | if (response.status === 200 && data.code === 0) {
18 | return data.result.phones;
19 | }
20 | return self.$q.reject(data.message);
21 | }
22 | }
23 |
24 | getPhoneDetail (id) {
25 | const self = this;
26 | return this.$http.get(`api/phones/${id}`)
27 | .then(_success)
28 | .catch(this[errorHandler]);
29 |
30 | function _success (response) {
31 | const data = response.data;
32 | if (response.status === 200 && data.code === 0) {
33 | return data.result.phone;
34 | }
35 | return self.$q.reject(data.message);
36 | }
37 | }
38 |
39 | addNewPhone (phone) {
40 | const self = this;
41 | const req = {
42 | phone
43 | };
44 | return this.$http.post('api/phones', req)
45 | .then(_success)
46 | .catch(this[errorHandler]);
47 |
48 | function _success (response) {
49 | const data = response.data;
50 | if (response.status === 200 && data.code === 0) {
51 | return data.result.phone;
52 | }
53 | return self.$q.reject(data.message);
54 | }
55 | }
56 |
57 | updatePhone (id, phone) {
58 | const self = this;
59 | const req = {
60 | phone
61 | };
62 | return this.$http.put(`api/phones/${id}`, req)
63 | .then(_success)
64 | .catch(this[errorHandler]);
65 |
66 | function _success (response) {
67 | const data = response.data;
68 | if (response.status === 200 && data.code === 0) {
69 | return data.result.phone;
70 | }
71 | return self.$q.reject(data.message);
72 | }
73 | }
74 |
75 | removePhone (id) {
76 | const self = this;
77 | return this.$http.delete(`api/phones/${id}`)
78 | .then(_success)
79 | .catch(this[errorHandler]);
80 |
81 | function _success (response) {
82 | const data = response.data;
83 | if (response.status === 200 && data.code === 0) {
84 | return data.result.phone;
85 | }
86 | return self.$q.reject(data.message);
87 | }
88 | }
89 | }
90 |
91 | PhoneService.$inject = ['$http', '$q', 'AjaxErrorHandler'];
92 |
93 | export default PhoneService;
94 |
--------------------------------------------------------------------------------
/source/test/e2e/mocks/e2e.config.js:
--------------------------------------------------------------------------------
1 | appTestConfig.$inject = ['$httpProvider'];
2 | function appTestConfig ($httpProvider) {
3 | $httpProvider.interceptors.push(apiDelayInterceptor);
4 |
5 | apiDelayInterceptor.$inject = ['$timeout', '$q'];
6 | function apiDelayInterceptor ($timeout, $q) {
7 | return {
8 | response (response) {
9 | // all API response will be delayed 1s to simulate real network
10 | const delay = 1000;
11 | if (response.config.url.match(/^api\//)) {
12 | const d = $q.defer();
13 | $timeout(() => {
14 | d.resolve(response);
15 | }, delay);
16 | return d.promise;
17 | }
18 | return response;
19 | }
20 | };
21 | }
22 | }
23 |
24 | export default appTestConfig;
25 |
--------------------------------------------------------------------------------
/source/test/e2e/mocks/e2e.data.js:
--------------------------------------------------------------------------------
1 | // all data used by e2e mock service is here
2 | class MockData {
3 | constructor () {
4 | // private
5 | this.userInfo = {
6 | name: 'PinkyJie'
7 | };
8 | this.userProducts = [
9 | {
10 | name: 'phone',
11 | count: 5,
12 | icon: 'mdi-hardware-phone-android'
13 | }
14 | ];
15 | this.phones = [
16 | {
17 | id: '1',
18 | model: 'iPhone 6',
19 | os: 'iOS',
20 | price: 5288,
21 | manufacturer: 'Apple',
22 | size: 4.7,
23 | releaseDate: _getTimestamp(2014, 10, 9)
24 | },
25 | {
26 | id: '2',
27 | model: 'iPhone 6 Plus',
28 | os: 'iOS',
29 | price: 6088,
30 | size: 5.5,
31 | manufacturer: 'Apple',
32 | releaseDate: _getTimestamp(2014, 10, 9)
33 | },
34 | {
35 | id: '3',
36 | model: 'Nexus 6',
37 | os: 'Android',
38 | price: 4400,
39 | size: 5.96,
40 | manufacturer: 'Motorola',
41 | releaseDate: _getTimestamp(2014, 10, 10)
42 | },
43 | {
44 | id: '4',
45 | model: 'Galaxy S6',
46 | os: 'Android',
47 | price: 5288,
48 | size: 5.1,
49 | manufacturer: 'Samsung',
50 | releaseDate: _getTimestamp(2015, 3, 25)
51 | },
52 | {
53 | id: '5',
54 | model: 'Mi Note',
55 | os: 'Android',
56 | price: 2299,
57 | size: 5.7,
58 | manufacturer: 'Xiaomi',
59 | releaseDate: _getTimestamp(2015, 1, 18)
60 | }
61 | ];
62 | }
63 | }
64 |
65 | MockData.$inject = [];
66 |
67 | function _getTimestamp (year, month, day) {
68 | const date = new Date();
69 | date.setFullYear(year);
70 | date.setMonth(month - 1);
71 | date.setDate(day);
72 | date.setHours(0);
73 | date.setMinutes(0);
74 | date.setSeconds(0);
75 | return date;
76 | }
77 |
78 | export default MockData;
79 |
--------------------------------------------------------------------------------
/source/test/e2e/mocks/e2e.phone.js:
--------------------------------------------------------------------------------
1 | // API mock for phone.service.js
2 | phoneServiceMock.$inject = ['MockData', '$httpBackend'];
3 | function phoneServiceMock (MockData, $httpBackend) {
4 | $httpBackend.whenGET('api/phones').respond(getPhonesHandler);
5 | $httpBackend.whenGET(/api\/phones\/\d+/).respond(getPhoneDetailHandler);
6 | $httpBackend.whenPOST('api/phones').respond(addNewPhoneHandler);
7 | $httpBackend.whenPUT(/api\/phones\/\d+/).respond(updatePhoneDetailHandler);
8 | $httpBackend.whenDELETE(/api\/phones\/\d+/).respond(removePhoneHandler);
9 |
10 | function getPhonesHandler () {
11 | return [200, {code: 0, message: null, result: {
12 | phones: MockData.phones
13 | }}];
14 | }
15 |
16 | function getPhoneDetailHandler (method, url) {
17 | const matches = url.match(/^api\/phones\/(\d+)/);
18 | let id;
19 | let targetPhone;
20 | if (matches.length === 2) {
21 | id = matches[1];
22 | targetPhone = _getPhoneById(id);
23 | if (targetPhone.length > 0) {
24 | return [200, {code: 0, message: null, result: {
25 | phone: targetPhone[0]
26 | }}];
27 | }
28 | }
29 | return [200, {code: 1, message: 'PHONE_QUERY_NOT_FOUND', result: null}];
30 | }
31 |
32 | function addNewPhoneHandler (method, url, data) {
33 | const req = JSON.parse(data);
34 | const currentCount = MockData.phones.length;
35 | req.phone.id = String(currentCount + 1);
36 | MockData.phones.push(req.phone);
37 | return [200, {code: 0, message: null, result: {
38 | phone: req.phone
39 | }}];
40 | }
41 |
42 | function updatePhoneDetailHandler (method, url, data) {
43 | const req = JSON.parse(data);
44 | const matches = url.match(/^api\/phones\/(\d+)/);
45 | let id;
46 | let targetPhone;
47 | let index;
48 | if (matches.length === 2) {
49 | id = matches[1];
50 | targetPhone = _getPhoneById(id);
51 | if (targetPhone.length > 0) {
52 | MockData.phones.forEach((phone, idx) => {
53 | if (phone.id === id) {
54 | index = idx;
55 | }
56 | });
57 | req.phone.id = id;
58 | MockData.phones[index] = req.phone;
59 | return [200, {code: 0, message: null, result: {
60 | phone: req.phone
61 | }}];
62 | }
63 | }
64 | return [200, {code: 1, message: 'PHONE_UPDATE_NOT_FOUND', result: null}];
65 | }
66 |
67 | function removePhoneHandler (method, url) {
68 | const matches = url.match(/^api\/phones\/(\d+)/);
69 | let id;
70 | let targetPhone;
71 | let index;
72 | if (matches.length === 2) {
73 | id = matches[1];
74 | targetPhone = _getPhoneById(id);
75 | if (targetPhone.length > 0) {
76 | MockData.phones.forEach((phone, idx) => {
77 | if (phone.id === id) {
78 | index = idx;
79 | }
80 | });
81 | MockData.phones.splice(index, 1);
82 | return [200, {code: 0, message: null, result: {
83 | phone: targetPhone[0]
84 | }}];
85 | }
86 | }
87 | return [200, {code: 1, message: 'PHONE_DELETE_NOT_FOUND', result: null}];
88 | }
89 |
90 | function _getPhoneById (id) {
91 | return MockData.phones.filter((phone) => {
92 | return phone.id === id;
93 | });
94 | }
95 | }
96 |
97 | export default phoneServiceMock;
98 |
--------------------------------------------------------------------------------
/source/test/e2e/mocks/e2e.user.js:
--------------------------------------------------------------------------------
1 | // API mock for user.service.js
2 | userServiceMock.$inject = ['MockData', '$httpBackend'];
3 | function userServiceMock (MockData, $httpBackend) {
4 | $httpBackend.whenGET('api/user/loginstatus').respond(loginStatusHandler);
5 | $httpBackend.whenPOST('api/user/login').respond(loginHandler);
6 | $httpBackend.whenPOST('api/user/logout').respond(logoutHandler);
7 | $httpBackend.whenGET('api/user/products').respond(productsHandler);
8 |
9 | function loginStatusHandler () {
10 | const code = MockData.loginStatus ? 0 : 1;
11 | const result = MockData.loginStatus ? {user: MockData.userInfo} : null;
12 | return [200, {code, message: null, result}];
13 | }
14 |
15 | function loginHandler (method, url, data) {
16 | const req = JSON.parse(data);
17 | if (req.email === 'error@error.com') {
18 | return [200, {
19 | code: 1,
20 | message: 'LOGIN_WRONG_EMAIL_PASSWORD_PAIR',
21 | result: null
22 | }];
23 | } else if (req.email === 'lock@lock.com') {
24 | return [200, {
25 | code: 1,
26 | message: 'LOGIN_USER_IN_LOCK',
27 | result: null
28 | }];
29 | }
30 | MockData.loginStatus = true;
31 | return [200, {
32 | code: 0, message: null,
33 | result: {
34 | user: MockData.userInfo
35 | }
36 | }];
37 | }
38 |
39 | function logoutHandler () {
40 | MockData.loginStatus = false;
41 | return [200, {code: 0, message: null, result: null}];
42 | }
43 |
44 | function productsHandler () {
45 | return [200, {code: 0, message: null, result: {
46 | summary: MockData.userProducts
47 | }}];
48 | }
49 | }
50 |
51 | export default userServiceMock;
52 |
--------------------------------------------------------------------------------
/source/test/e2e/mocks/index.js:
--------------------------------------------------------------------------------
1 | import angular from 'angular';
2 | import 'angular-mocks';
3 |
4 | import app from '../../../app';
5 |
6 | import MockData from './e2e.data';
7 | import appTestConfig from './e2e.config';
8 | import userServiceMock from './e2e.user';
9 | import phoneServiceMock from './e2e.phone';
10 |
11 | angular.module('appTest', [
12 | app.name,
13 | 'ngMockE2E'
14 | ])
15 | .service(MockData.name, MockData)
16 | .config(appTestConfig)
17 | .run(userServiceMock)
18 | .run(phoneServiceMock);
19 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/404.spec.js:
--------------------------------------------------------------------------------
1 | import NotFoundPage from './page-objects/404.page';
2 |
3 | describe('404 Page:', () => {
4 | let page;
5 | beforeEach(() => {
6 | page = new NotFoundPage();
7 | page.load();
8 | });
9 |
10 | describe('Layout:', () => {
11 | it('should have correct layout', () => {
12 | const config = {
13 | url: '404',
14 | title: '404',
15 | klass: 'notfound',
16 | header: 'prelogin',
17 | sidebar: false,
18 | breadcrumb: {
19 | items: [
20 | {
21 | link: false,
22 | text: '404'
23 | }
24 | ]
25 | }
26 | };
27 | page.assertCorrectLayout(config);
28 | });
29 | });
30 |
31 | describe('Main section:', () => {
32 | it('should display 404 text', () => {
33 | expect(page.ele.text.getText()).toEqual('404 Not Found');
34 | });
35 | });
36 |
37 | it('should redirect user to 404 with non-exist URL', () => {
38 | browser._.gotoUrl('aaa');
39 | browser._.expectUrlToMatch('404');
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/dashboard.spec.js:
--------------------------------------------------------------------------------
1 | import DashboardPage from './page-objects/dashboard.page';
2 |
3 | describe('Dashboard Page:', () => {
4 | let page;
5 | beforeEach(() => {
6 | page = new DashboardPage();
7 | page.load();
8 | });
9 |
10 | describe('Layout:', () => {
11 | it('should have correct layout', () => {
12 | const config = {
13 | url: 'dashboard',
14 | title: 'Dashboard',
15 | klass: 'dashboard',
16 | header: 'login',
17 | sidebar: {
18 | items: [
19 | {
20 | link: 'dashboard',
21 | active: true,
22 | icon: 'mdi-action-dashboard',
23 | text: 'DASHBOARD'
24 | },
25 | {
26 | link: 'phone',
27 | active: false,
28 | icon: 'mdi-hardware-phone-android',
29 | text: 'PHONES'
30 | }
31 | ]
32 | },
33 | breadcrumb: {
34 | items: [
35 | {
36 | link: false,
37 | text: 'Dashboard'
38 | }
39 | ]
40 | }
41 | };
42 | page.assertCorrectLayout(config);
43 | });
44 | });
45 |
46 | describe('Banner section:', () => {
47 | it('should have correct banner text', () => {
48 | expect(page.ele.bannerText.getText()).toEqual('Welcome PinkyJie!');
49 | });
50 | });
51 |
52 | describe('Menu Box section:', () => {
53 | it('should display correct menu box', () => {
54 | const colors = ['indigo', 'red', 'pink'];
55 | const boxes = page.ele.menuBox;
56 | const expectedMenus = [
57 | {
58 | icon: 'mdi-hardware-phone-android',
59 | count: '5',
60 | name: 'PHONE',
61 | link: 'phone'
62 | }
63 | ];
64 | expect(boxes.view.count()).toBe(1);
65 | boxes.view.each((box, index) => {
66 | const expectedMenu = expectedMenus[index];
67 | expect(box).toHaveClass(colors[index % colors.length]);
68 | expect(box.getAttribute('href')).toEqual(`${browser.baseUrl}/${expectedMenu.link}`);
69 | expect(box.$(boxes.icon)).toHaveClass(expectedMenu.icon);
70 | expect(box.$(boxes.name).getText()).toEqual(expectedMenu.name);
71 | expect(box.$(boxes.count).getText()).toEqual(expectedMenu.count);
72 | });
73 | });
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/home.spec.js:
--------------------------------------------------------------------------------
1 | import HomePage from './page-objects/home.page';
2 |
3 | describe('Home Page:', () => {
4 | let page;
5 | beforeEach(() => {
6 | page = new HomePage();
7 | page.load();
8 | });
9 |
10 | describe('Layout:', () => {
11 | it('should have correct layout', () => {
12 | const config = {
13 | url: '/',
14 | title: 'Home',
15 | klass: 'home',
16 | header: 'prelogin',
17 | sidebar: false,
18 | breadcrumb: false
19 | };
20 | page.assertCorrectLayout(config);
21 | });
22 | });
23 |
24 | describe('Hero section:', () => {
25 | it('should display correct title and sub title', () => {
26 | expect(page.ele.mainTitle.getText()).toEqual('Aio Angular App');
27 | expect(page.ele.subTitle.getText()).toEqual(
28 | 'Awesome web app built on AngularJS & Material Design.');
29 | });
30 |
31 | it('should go to login page if click get started', () => {
32 | page.ele.getStartedBtn.click();
33 | browser._.expectUrlToMatch('login');
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/login.spec.js:
--------------------------------------------------------------------------------
1 | import LoginPage from './page-objects/login.page';
2 |
3 | describe('Login Page:', () => {
4 | let page;
5 | beforeEach(() => {
6 | page = new LoginPage();
7 | page.load();
8 | });
9 |
10 | describe('Layout:', () => {
11 | it('should have correct layout', () => {
12 | const config = {
13 | url: 'login',
14 | title: 'Login',
15 | klass: 'login',
16 | header: 'prelogin',
17 | sidebar: false,
18 | breadcrumb: false
19 | };
20 | page.assertCorrectLayout(config);
21 | });
22 | });
23 |
24 | describe('Login form section:', () => {
25 | it('should login successfully with correct credential', () => {
26 | expect(page.ele.loginBtn.isEnabled()).toBe(false);
27 | page.loginWithCredential('f@f', 'f');
28 | browser._.expectUrlToMatch(page.urlAfterLogin);
29 | });
30 |
31 | it('should display error message with incorrect credential', () => {
32 | expect(page.ele.loginMessage.isPresent()).toBe(false);
33 | page.loginWithCredential('error@error.com', 'f');
34 | expect(page.ele.loginMessage.isDisplayed()).toBe(true);
35 | expect(page.ele.loginMessage.getText())
36 | .toEqual('Incorrect email or password, please try again!');
37 | });
38 |
39 | it('should display error message with locked account', () => {
40 | expect(page.ele.loginMessage.isPresent()).toBe(false);
41 | page.loginWithCredential('lock@lock.com', 'f');
42 | expect(page.ele.loginMessage.isDisplayed()).toBe(true);
43 | expect(page.ele.loginMessage.getText())
44 | .toEqual('Your account is locked!');
45 | });
46 |
47 | it('should automatically log user to dashboard if user has already logged in', () => {
48 | page.loginWithCredential('f@f', 'f');
49 | browser._.expectUrlToMatch(page.urlAfterLogin);
50 | // go back to home page
51 | page.getHeader().title.click();
52 | page.homePage.ele.getStartedBtn.click();
53 | // go to login page will redirect user to dashboard
54 | browser._.expectUrlToMatch(page.urlAfterLogin);
55 | });
56 | });
57 | });
58 |
59 | describe('Logout Page:', () => {
60 | let page;
61 | beforeEach(() => {
62 | page = new LoginPage();
63 | browser._.gotoUrl(page.logoutUrl);
64 | });
65 |
66 | it('should not display loading view', () => {
67 | expect(page.ele.loadingView.isPresent()).toBe(false);
68 | });
69 |
70 | it('should display success logout message', () => {
71 | expect(page.ele.loginMessage.isDisplayed()).toBe(true);
72 | expect(page.ele.loginMessage.getText())
73 | .toEqual('You have been successfully logged out!');
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/page-objects/404.page.js:
--------------------------------------------------------------------------------
1 | // 404 page object
2 | class NotFoundPage extends browser._BasePageObject {
3 | constructor () {
4 | super('404');
5 | }
6 |
7 | _getAllElements () {
8 | const $page = $('.not-found-view');
9 | return {
10 | text: $page.$('h2')
11 | };
12 | }
13 | }
14 |
15 | export default NotFoundPage;
16 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/page-objects/dashboard.page.js:
--------------------------------------------------------------------------------
1 | import LoginPage from './login.page';
2 |
3 | // dashboard page object
4 | class DashboardPage extends browser._BasePageObject {
5 | constructor () {
6 | super('dashboard');
7 | }
8 |
9 | _getAllElements () {
10 | const $page = $('.dashboard-view');
11 | const bannerClass = '.banner-view';
12 | const menuBoxClass = '.square-menu-view';
13 | return {
14 | banner: $page.$(bannerClass),
15 | bannerText: $page.$(`${bannerClass} p`),
16 | menuBoxes: $page.$(menuBoxClass),
17 | menuBox: {
18 | view: $page.$$(`${menuBoxClass} > .box`),
19 | icon: 'i',
20 | name: '.name',
21 | count: '.count'
22 | }
23 | };
24 | }
25 |
26 | // overrite load function to support login
27 | load () {
28 | super.load();
29 | const loginPage = new LoginPage();
30 | browser._.expectUrlToMatch(loginPage.url);
31 | loginPage.loginWithCredential('f@f', 'f');
32 | browser._.expectUrlToMatch(this.url);
33 | }
34 | }
35 |
36 | export default DashboardPage;
37 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/page-objects/home.page.js:
--------------------------------------------------------------------------------
1 | // home page object
2 | class HomePage extends browser._BasePageObject {
3 | constructor () {
4 | super('');
5 | }
6 |
7 | _getAllElements () {
8 | const $page = $('.home-view');
9 | return {
10 | mainTitle: $page.$('.title'),
11 | subTitle: $page.$('.subtitle'),
12 | getStartedBtn: $page.$('.btn-get-started')
13 | };
14 | }
15 | }
16 |
17 | export default HomePage;
18 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/page-objects/login.page.js:
--------------------------------------------------------------------------------
1 | import HomePage from './home.page';
2 |
3 | // login page object
4 | class LoginPage extends browser._BasePageObject {
5 | constructor () {
6 | super('login');
7 | this.urlAfterLogin = 'dashboard';
8 | this.logoutUrl = 'login?action=logout';
9 | this.homePage = new HomePage();
10 | }
11 |
12 | _getAllElements () {
13 | const $page = $('.login-view');
14 | return {
15 | loadingView: $page.$('.login-checking'),
16 | loginMessage: $page.$('.login-message > p'),
17 | emailInput: $page.element(by.model('form.credential.email')),
18 | passwordInput: $page.element(by.model('form.credential.password')),
19 | loginBtn: $page.$('.btn-login')
20 | };
21 | }
22 |
23 | loginWithCredential (email, password) {
24 | this.ele.emailInput.sendKeys(email);
25 | this.ele.passwordInput.sendKeys(password);
26 | expect(this.ele.loginBtn.isEnabled()).toBe(true);
27 | this.ele.loginBtn.click();
28 | }
29 | }
30 |
31 | export default LoginPage;
32 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/page-objects/phone-add.page.js:
--------------------------------------------------------------------------------
1 | import LoginPage from './login.page';
2 | import PhoneFormComp from './phone-form.comp';
3 | import PhonePage from './phone-main.page';
4 |
5 | // phone add page object
6 | class PhoneAddPage extends browser._BasePageObject {
7 | constructor () {
8 | super('phone/add');
9 | this.data = {
10 | phone: {
11 | Model: '',
12 | OS: 'Choose OS Type',
13 | Price: '',
14 | 'Screen Size': '',
15 | Manufacturer: '',
16 | 'Release Date': '',
17 | date: ''
18 | }
19 | };
20 | this.phonePage = new PhonePage();
21 | }
22 |
23 | _getAllElements () {
24 | const $page = $('.phone-add-view');
25 | const $header = $page.$('.card-header');
26 | return {
27 | title: $header.$('.title'),
28 | form: new PhoneFormComp($page)
29 | };
30 | }
31 |
32 | // overrite load function to support login
33 | load () {
34 | super.load();
35 | const loginPage = new LoginPage();
36 | browser._.expectUrlToMatch(loginPage.url);
37 | loginPage.loginWithCredential('f@f', 'f');
38 | browser._.expectUrlToMatch(this.url);
39 | }
40 | }
41 |
42 | export default PhoneAddPage;
43 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/page-objects/phone-detail.page.js:
--------------------------------------------------------------------------------
1 | import LoginPage from './login.page';
2 | import PhoneFormComp from './phone-form.comp';
3 |
4 | // phone detail page object
5 | class PhoneDetailPage extends browser._BasePageObject {
6 | constructor () {
7 | super('phone/1');
8 | this.data = {
9 | phone: {
10 | Model: 'iPhone 6',
11 | OS: 'iOS',
12 | Price: '5288',
13 | 'Screen Size': '4.7',
14 | Manufacturer: 'Apple',
15 | 'Release Date': 'October 9, 2014',
16 | date: '2014-10-9'
17 | }
18 | };
19 | }
20 |
21 | _getAllElements () {
22 | const $page = $('.phone-detail-view');
23 | const $header = $page.$('.card-header');
24 | return {
25 | title: $header.$('.title'),
26 | editBtn: $header.$('button'),
27 | form: new PhoneFormComp($page)
28 | };
29 | }
30 |
31 | // overrite load function to support login
32 | load () {
33 | super.load();
34 | const loginPage = new LoginPage();
35 | browser._.expectUrlToMatch(loginPage.url);
36 | loginPage.loginWithCredential('f@f', 'f');
37 | browser._.expectUrlToMatch(this.url);
38 | }
39 | }
40 |
41 | export default PhoneDetailPage;
42 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/page-objects/phone-main.page.js:
--------------------------------------------------------------------------------
1 | import LoginPage from './login.page';
2 |
3 | // phone page object
4 | class PhonePage extends browser._BasePageObject {
5 | constructor () {
6 | super('phone');
7 | }
8 |
9 | _getAllElements () {
10 | const $page = $('.phone-main-view');
11 | return {
12 | addNewBtn: $page.$('.btn-add-new'),
13 | tableHeader: $page.$$('.heading th'),
14 | phoneItem: {
15 | view: $page.$$('.phone-item'),
16 | cell: 'td',
17 | firstBtn: '.btn-first',
18 | secondBtn: '.btn-second',
19 | icon: 'i'
20 | }
21 | };
22 | }
23 |
24 | // overrite load function to support login
25 | load () {
26 | super.load();
27 | const loginPage = new LoginPage();
28 | browser._.expectUrlToMatch(loginPage.url);
29 | loginPage.loginWithCredential('f@f', 'f');
30 | browser._.expectUrlToMatch(this.url);
31 | }
32 | }
33 |
34 | export default PhonePage;
35 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/phone-add.spec.js:
--------------------------------------------------------------------------------
1 | import PhoneAddPage from './page-objects/phone-add.page';
2 |
3 | describe('Phone Add Page:', () => {
4 | let page;
5 | beforeEach(() => {
6 | page = new PhoneAddPage();
7 | page.load();
8 | });
9 |
10 | describe('Layout:', () => {
11 | it('should have correct layout', () => {
12 | const config = {
13 | url: 'phone/add',
14 | title: 'Phone',
15 | klass: 'phone',
16 | header: 'login',
17 | sidebar: {
18 | items: [
19 | {
20 | link: 'dashboard',
21 | active: false,
22 | icon: 'mdi-action-dashboard',
23 | text: 'DASHBOARD'
24 | },
25 | {
26 | link: 'phone',
27 | active: true,
28 | icon: 'mdi-hardware-phone-android',
29 | text: 'PHONES'
30 | }
31 | ]
32 | },
33 | breadcrumb: {
34 | items: [
35 | {
36 | link: 'phone',
37 | text: 'Phone List'
38 | },
39 | {
40 | link: false,
41 | text: 'Add Phone'
42 | }
43 | ]
44 | }
45 | };
46 | page.assertCorrectLayout(config);
47 | });
48 | });
49 |
50 | describe('Phone Add section:', () => {
51 | it('should have correct initial state', () => {
52 | expect(page.ele.title.getText()).toEqual('Add a new phone');
53 | expect(page.ele.form.ele.cancelBtn.isDisplayed()).toBe(false);
54 | expect(page.ele.form.ele.saveBtn.isEnabled()).toBe(false);
55 | page.ele.form.assertEditPhoneDetail(page.data.phone);
56 | });
57 |
58 | it('should add phone correctly via form', () => {
59 | const now = new Date();
60 | const newPhone = {
61 | Model: 'iPhone 17',
62 | OS: 'iOS',
63 | Price: '9999',
64 | 'Screen Size': '9.9',
65 | Manufacturer: 'AppleApple',
66 | 'Release Date': 'November 20, 2015',
67 | // always use today's date for easy testing
68 | date: `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`
69 | };
70 | page.ele.form.assertEditingForm(newPhone, true);
71 | // back to phone main page
72 | browser._.expectUrlToMatch(page.phonePage.url);
73 | // check last item is the new added item
74 | const phoneList = page.phonePage.ele.phoneItem;
75 | expect(phoneList.view.count()).toEqual(6);
76 | const lastItem = phoneList.view.get(5);
77 | const expectedItem = [newPhone.Model, newPhone.OS, newPhone.Price];
78 | browser._.isMobile().then((isSmall) => {
79 | lastItem.$$(phoneList.cell).each((td, index) => {
80 | if (index === 3) {
81 | // ignore the last cell
82 | return;
83 | }
84 | if (isSmall && (index === 1 || index === 2)) {
85 | expect(td.isDisplayed()).toBe(false);
86 | } else {
87 | expect(td.getText()).toEqual(expectedItem[index]);
88 | }
89 | });
90 | });
91 | });
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/source/test/e2e/specs/phone-detail.spec.js:
--------------------------------------------------------------------------------
1 | import PhoneDetailPage from './page-objects/phone-detail.page';
2 |
3 | describe('Phone Detail Page:', () => {
4 | let page;
5 | beforeEach(() => {
6 | page = new PhoneDetailPage();
7 | page.load();
8 | });
9 |
10 | describe('Layout:', () => {
11 | it('should have correct layout', () => {
12 | const config = {
13 | url: 'phone/1',
14 | title: 'Phone',
15 | klass: 'phone',
16 | header: 'login',
17 | sidebar: {
18 | items: [
19 | {
20 | link: 'dashboard',
21 | active: false,
22 | icon: 'mdi-action-dashboard',
23 | text: 'DASHBOARD'
24 | },
25 | {
26 | link: 'phone',
27 | active: true,
28 | icon: 'mdi-hardware-phone-android',
29 | text: 'PHONES'
30 | }
31 | ]
32 | },
33 | breadcrumb: {
34 | items: [
35 | {
36 | link: 'phone',
37 | text: 'Phone List'
38 | },
39 | {
40 | link: false,
41 | text: 'Phone Detail'
42 | }
43 | ]
44 | }
45 | };
46 | page.assertCorrectLayout(config);
47 | });
48 | });
49 |
50 | describe('Phone Detail section:', () => {
51 | it('should show correct phone detail', () => {
52 | expect(page.ele.title.getText()).toEqual(page.data.phone.Model);
53 | expect(page.ele.editBtn.$('i')).toHaveClass('mdi-image-edit');
54 | page.ele.form.assertPhoneDetail(page.data.phone);
55 | });
56 | });
57 |
58 | describe('Phone Edit section:', () => {
59 | let formField;
60 | beforeEach(() => {
61 | formField = page.ele.form.ele.field;
62 | });
63 |
64 | it('should show phone edit view correctly', () => {
65 | expect(formField.view.get(0).isDisplayed()).toBe(false);
66 | expect(page.ele.form.ele.cancelBtn.isDisplayed()).toBe(false);
67 | expect(page.ele.form.ele.saveBtn.isDisplayed()).toBe(false);
68 | // click edit
69 | page.ele.editBtn.click();
70 | expect(formField.view.get(0).isDisplayed()).toBe(true);
71 | expect(page.ele.form.ele.cancelBtn.isDisplayed()).toBe(true);
72 | expect(page.ele.form.ele.saveBtn.isDisplayed()).toBe(true);
73 | expect(page.ele.form.ele.saveBtn.isEnabled()).toBe(false);
74 | // check label/value
75 | page.ele.form.assertEditPhoneDetail(page.data.phone);
76 | });
77 |
78 | it('should cancel editing after clicking cancel button', () => {
79 | // click edit
80 | page.ele.editBtn.click();
81 | // edit model
82 | const modelInput = formField.view.get(0).$(formField.input);
83 | modelInput.clear();
84 | modelInput.sendKeys('abcdefg');
85 | // click cancel
86 | page.ele.form.ele.cancelBtn.click();
87 | // check phone detail not changed
88 | page.ele.form.assertPhoneDetail(page.data.phone);
89 | });
90 |
91 | it('should allow editing every field in form', () => {
92 | const newPhone = {
93 | Model: 'iPhone 6 123',
94 | OS: 'Android',
95 | Price: '1234',
96 | 'Screen Size': '12.3',
97 | Manufacturer: 'abcd',
98 | 'Release Date': 'October 17, 2014',
99 | date: '2014-10-17'
100 | };
101 | page.ele.editBtn.click();
102 | page.ele.form.assertEditingForm(newPhone, false);
103 | // check new phone detail
104 | page.ele.form.assertPhoneDetail(newPhone);
105 | });
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/source/test/unit/helper.js:
--------------------------------------------------------------------------------
1 | import 'angular';
2 | import 'angular-mocks';
3 | import 'materialize-css/dist/js/materialize.js';
4 |
5 | const testsContext = require.context('../../app', true, /\.spec\.js$/);
6 | testsContext.keys().forEach(testsContext);
7 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var ExtractTextPlugin = require("extract-text-webpack-plugin");
3 | var HtmlWebpackPlugin = require('html-webpack-plugin');
4 | var CopyWebpackPlugin = require('copy-webpack-plugin');
5 | var autoprefixer = require('autoprefixer');
6 | var args = require('yargs').argv;
7 |
8 | // parameters
9 | var isProd = args.prod;
10 | var isMock = args.mock;
11 |
12 | var base = './';
13 | // use mock api or not
14 | var entryJs = isMock ?
15 | base + 'source/test/e2e/mocks/index.js' :
16 | base + 'source/app/index.js';
17 | var appName = isMock ? 'appTest' : 'app';
18 |
19 | var plugins = [
20 | new webpack.ProvidePlugin({
21 | $: "jquery",
22 | jQuery: "jquery",
23 | // materialize-css rely on this to support velocity
24 | "window.jQuery": "jquery"
25 | }),
26 | new webpack.DefinePlugin({
27 | __PROD__: isProd,
28 | __MOCK__: isMock
29 | }),
30 | new webpack.optimize.CommonsChunkPlugin('vendor', isProd ? 'vendor.[hash].js' : 'vendor.js'),
31 | new ExtractTextPlugin(isProd ? '[name].[hash].css' : '[name].css'),
32 | new HtmlWebpackPlugin({
33 | template: 'jade!./source/app/index.jade',
34 | chunks: ['app', 'vendor'],
35 | favicon: 'favicon.ico',
36 | appName: appName
37 | }),
38 | new CopyWebpackPlugin([
39 | { from: 'node_modules/babel-core/browser-polyfill.min.js', to: 'polyfill.js'}
40 | ])
41 | ];
42 |
43 | if (isProd) {
44 | plugins.push(
45 | new webpack.NoErrorsPlugin(),
46 | new webpack.optimize.DedupePlugin(),
47 | new webpack.optimize.UglifyJsPlugin({
48 | compress: {
49 | warnings: false
50 | },
51 | mangle: false
52 | }),
53 | new webpack.optimize.OccurenceOrderPlugin()
54 | );
55 | }
56 |
57 | module.exports = {
58 | entry: {
59 | app: [
60 | entryJs
61 | ],
62 | vendor: [
63 | // 3rd dependencies
64 | 'materialize-css/bin/materialize.css',
65 | 'materialize-css/bin/materialize.js',
66 | 'animate.css/animate.css',
67 | // angular
68 | 'angular',
69 | 'angular-ui-router',
70 | 'angular-animate',
71 | 'angular-messages',
72 | 'angular-mocks',
73 | 'angular-loading-bar',
74 | 'oclazyload'
75 | ]
76 | },
77 | output: {
78 | path: base + 'build',
79 | filename: isProd ? '[name].[hash].js' : '[name].js',
80 | chunkFilename: isProd ? '[name].[hash].chunk.js' : '[name].chunk.js'
81 | },
82 | module: {
83 | preLoaders: [
84 | {
85 | test: /\.js$/,
86 | loader: "eslint",
87 | exclude: /node_modules/
88 | }
89 | ],
90 | loaders: [
91 | {
92 | test: /\.js$/,
93 | loader: 'babel',
94 | exclude: /node_modules/
95 | },
96 | {
97 | test: /\.html$/,
98 | loader: 'html'
99 | },
100 | {
101 | test: /\.jade$/,
102 | loader: 'jade',
103 | exclude: /index\.jade/
104 | },
105 | {
106 | test: /\.styl$/,
107 | loader: ExtractTextPlugin.extract('style', 'css?sourceMap!postcss!stylus')
108 | },
109 | {
110 | test: /\.css$/,
111 | loader: ExtractTextPlugin.extract('style', 'css?sourceMap')
112 | },
113 | {
114 | test: /\.(woff|woff2|ttf|eot|svg)(\?]?.*)?$/,
115 | loader : 'file?name=assets/fonts/[name].[ext]?[hash]'
116 | },
117 | {
118 | test: /\.(png|jpg)$/,
119 | loader: 'url?limit=8192&name=assets/images/[name].[hash].[ext]'
120 | }
121 | ]
122 | },
123 | plugins: plugins,
124 | debug: !isProd,
125 | devtool: isProd ? 'source-map' : 'eval-source-map',
126 | devServer: {
127 | contentBase: base + 'build',
128 | historyApiFallback: true,
129 | stats: {
130 | modules: false,
131 | cached: false,
132 | colors: true,
133 | chunk: false
134 | },
135 | host: '0.0.0.0',
136 | port: 8080
137 | },
138 | postcss: function () {
139 | return [autoprefixer];
140 | }
141 | };
142 |
--------------------------------------------------------------------------------